beast-agent 0.25.5 → 0.25.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1535,6 +1535,35 @@ async function renderIntegrationsPane() {
1535
1535
  <button id="tgAllowAdd" class="btn ghost" style="margin-top:6px">${_t('it_add')}</button>
1536
1536
  </div>
1537
1537
  <div class="sub" style="margin-top:8px">${_t('it_tg_note')}</div>
1538
+ </div>
1539
+
1540
+ <div class="wa-card" style="margin-top:14px">
1541
+ <div class="wa-head">
1542
+ <div class="wa-logo tg">D</div>
1543
+ <div>
1544
+ <div class="wa-title">Discord</div>
1545
+ <div class="wa-sub">${_t('it_dc_sub')}</div>
1546
+ </div>
1547
+ </div>
1548
+ <div class="wa-status"><span id="dcDot" class="wa-dot"></span><span id="dcStatText">—</span></div>
1549
+ <div id="dcUser" class="wa-user" hidden></div>
1550
+ <div class="form-grid" style="grid-template-columns:1fr auto;align-items:center">
1551
+ <input id="dcTokenInp" class="inp" type="password" style="margin:6px 0 0" placeholder="${_t('it_dc_token_ph')}" autocomplete="off" />
1552
+ <button id="dcSaveBtn" class="btn" style="margin-top:6px">${_t('it_tg_save')}</button>
1553
+ </div>
1554
+ <div class="wa-actions" style="margin-top:6px">
1555
+ <button id="dcStopBtn" class="btn ghost">${_t('it_disconnect')}</button>
1556
+ </div>
1557
+ <div class="divider"></div>
1558
+ <label class="mem-label" style="margin-top:0">${_t('it_allow_label')} — Discord</label>
1559
+ <div id="dcAllowChips" class="chips-inline"></div>
1560
+ <div class="form-grid" style="grid-template-columns:1.2fr 1fr 1fr auto;align-items:center">
1561
+ <input id="dcAllowNameInp" class="inp" style="margin:6px 0 0" placeholder="${_t('it_name_ph')}" autocomplete="off" />
1562
+ <input id="dcAllowIdInp" class="inp" style="margin:6px 0 0" placeholder="${_t('it_dc_id_ph')}" autocomplete="off" />
1563
+ <select id="dcAllowBotSel" class="perm-select" style="margin:6px 0 0;min-width:105px" title="${_t('bot_bind_title')}"></select>
1564
+ <button id="dcAllowAdd" class="btn ghost" style="margin-top:6px">${_t('it_add')}</button>
1565
+ </div>
1566
+ <div class="sub" style="margin-top:8px">${_t('it_dc_note')}</div>
1538
1567
  </div>`;
1539
1568
 
1540
1569
  const waGroupsOn = $('#waGroupsOn');
@@ -1597,6 +1626,29 @@ async function renderIntegrationsPane() {
1597
1626
  tgUI.user = ts.user || null;
1598
1627
  updateTgPane();
1599
1628
  } catch {}
1629
+
1630
+ /* DISCORD: token + allow list + durum */
1631
+ $('#dcSaveBtn').addEventListener('click', async () => {
1632
+ const tok = $('#dcTokenInp').value.trim();
1633
+ if (!tok) { toast(_t('it_dc_token_ph')); return; }
1634
+ toast(_t('it_dc_connecting'));
1635
+ const r = await beast.dcSetToken(tok).catch((e) => ({ status: 'error', error: String(e) }));
1636
+ $('#dcTokenInp').value = '';
1637
+ updateDcPane();
1638
+ toast(r.status === 'connected' ? 'Discord bağlı: ' + (r.user || '') : r.status === 'error' ? _t('it_dc_token_bad') : _t('it_dc_connecting'));
1639
+ });
1640
+ $('#dcStopBtn').addEventListener('click', async () => {
1641
+ await beast.dcStop().catch(() => {});
1642
+ updateDcPane();
1643
+ toast('Kesildi');
1644
+ });
1645
+ await renderDcAllow();
1646
+ try {
1647
+ const ds = await beast.dcGetStatus();
1648
+ dcUI.status = ds.status || 'disconnected';
1649
+ dcUI.user = ds.user || null;
1650
+ updateDcPane();
1651
+ } catch {}
1600
1652
  }
1601
1653
 
1602
1654
  /* ---------------- Olay Merkezi (ayrı sekme) ---------------- */
@@ -2439,6 +2491,199 @@ async function renderTgAllow() {
2439
2491
  }
2440
2492
  }
2441
2493
 
2494
+ /* ---------------- Discord izin listesi + durum — TG ile aynı mantık ---------------- */
2495
+
2496
+ const dcUI = { status: 'disconnected', user: null };
2497
+ const DC_STATUS_TEXT = {
2498
+ disconnected: 'Bağlı değil',
2499
+ connecting: 'Bağlanıyor…',
2500
+ connected: 'Bağlı',
2501
+ error: 'Hata — tokenı kontrol et',
2502
+ };
2503
+
2504
+ function onDcEvent(ev) {
2505
+ if (ev.type !== 'status') return;
2506
+ dcUI.status = ev.status;
2507
+ if (ev.user) dcUI.user = ev.user;
2508
+ updateDcPane();
2509
+ }
2510
+
2511
+ function updateDcPane() {
2512
+ const pane = $('#tab-integrations');
2513
+ const dot = pane && pane.querySelector('#dcDot');
2514
+ if (!dot) return;
2515
+ dot.className = 'wa-dot' + (dcUI.status === 'connected' ? ' on' : dcUI.status === 'error' ? ' qr' : '');
2516
+ pane.querySelector('#dcStatText').textContent = DC_STATUS_TEXT[dcUI.status] || dcUI.status;
2517
+ const u = pane.querySelector('#dcUser');
2518
+ u.hidden = !(dcUI.status === 'connected' && dcUI.user);
2519
+ u.textContent = dcUI.user ? '🤖 ' + dcUI.user : '';
2520
+ }
2521
+
2522
+ async function renderDcAllow() {
2523
+ const wrap = $('#dcAllowChips');
2524
+ if (!wrap) return;
2525
+ const list = await beast.dcGetAllow();
2526
+ let botChoices = [];
2527
+ try { botChoices = (await beast.botsList()) || []; } catch {}
2528
+ const dcLabel = (e) => {
2529
+ if (e === '*') return '* herkes';
2530
+ if (typeof e === 'string') return e;
2531
+ const name = String((e && e.name) || '').trim();
2532
+ const id = String((e && e.id) || '');
2533
+ const bot = e && e.bot_id ? botChoices.find((bb) => bb.id === e.bot_id) : null;
2534
+ return (name ? name + ' ' : '') + id + (bot ? ' → ' + bot.name : '');
2535
+ };
2536
+ wrap.innerHTML = '';
2537
+ if (!list.length) wrap.innerHTML = '<span class="sub">— boş —</span>';
2538
+ list.forEach((entry, idx) => {
2539
+ const curPerm = (typeof entry === 'object' && entry.perm) || (entry && entry.lockdown ? 'chat' : 'all');
2540
+ const c = document.createElement('span');
2541
+ c.className = 'chip';
2542
+ c.style.margin = '0 6px 6px 0';
2543
+ const txt = document.createElement('span');
2544
+ txt.className = 'chip-txt';
2545
+ txt.textContent = dcLabel(entry);
2546
+ c.appendChild(txt);
2547
+ if (entry !== '*') {
2548
+ const selP = document.createElement('select');
2549
+ selP.className = 'perm-select';
2550
+ selP.title = _t('wa_perm_title');
2551
+ const PERMS = [
2552
+ ['all', _t('wa_perm_all')],
2553
+ ['web', 'web'],
2554
+ ['read', _t('wa_perm_read')],
2555
+ ['chat', _t('wa_perm_chat')],
2556
+ ];
2557
+ for (const [v, lbl] of PERMS) {
2558
+ const o = document.createElement('option');
2559
+ o.value = v;
2560
+ o.textContent = lbl;
2561
+ if (curPerm === v) o.selected = true;
2562
+ selP.appendChild(o);
2563
+ }
2564
+ selP.addEventListener('change', async () => {
2565
+ const cur = await beast.dcGetAllow();
2566
+ const next = cur.map((e, i) => {
2567
+ if (i !== idx) return e;
2568
+ const base = typeof e === 'string' ? { id: e, name: '' } : { ...e };
2569
+ base.lockdown = selP.value === 'chat';
2570
+ base.perm = selP.value;
2571
+ return base;
2572
+ });
2573
+ await beast.dcSetAllow(next);
2574
+ renderDcAllow();
2575
+ toast(dcLabel(entry) + ': ' + selP.selectedOptions[0].textContent);
2576
+ });
2577
+ c.appendChild(selP);
2578
+
2579
+ if (botChoices.length) {
2580
+ const sel = document.createElement('select');
2581
+ sel.className = 'perm-select';
2582
+ sel.title = _t('bot_bind_title');
2583
+ const empty = document.createElement('option');
2584
+ empty.value = '';
2585
+ empty.textContent = _t('bot_sel_empty');
2586
+ sel.appendChild(empty);
2587
+ for (const bb of botChoices) {
2588
+ const o = document.createElement('option');
2589
+ o.value = bb.id;
2590
+ o.textContent = `${bb.icon} ${bb.name}`;
2591
+ if ((typeof entry === 'object' && entry.bot_id) === bb.id) o.selected = true;
2592
+ sel.appendChild(o);
2593
+ }
2594
+ sel.addEventListener('change', async () => {
2595
+ const cur = await beast.dcGetAllow();
2596
+ const next = cur.map((e, i) => {
2597
+ if (i !== idx) return e;
2598
+ const base = typeof e === 'string' ? { id: e, name: '' } : { ...e };
2599
+ base.bot_id = sel.value || undefined;
2600
+ return base;
2601
+ });
2602
+ await beast.dcSetAllow(next);
2603
+ renderDcAllow();
2604
+ const bt = botChoices.find((bb) => bb.id === sel.value);
2605
+ toast(dcLabel(entry) + ' → ' + (bt ? bt.name : '?'));
2606
+ });
2607
+ c.appendChild(sel);
2608
+ }
2609
+
2610
+ const isOwnerEntry = typeof entry === 'object' && !!entry.owner;
2611
+ const ownerBtn = document.createElement('span');
2612
+ ownerBtn.className = 'lk';
2613
+ ownerBtn.style.cssText = 'font-size:11px;padding:1px 6px;' + (isOwnerEntry ? 'color:#c9a227;font-weight:800' : '');
2614
+ ownerBtn.title = isOwnerEntry ? _t('wa_owner_title_on') : _t('wa_owner_title_off');
2615
+ ownerBtn.textContent = isOwnerEntry ? _t('wa_owner_on') : _t('wa_owner_off');
2616
+ ownerBtn.addEventListener('click', async () => {
2617
+ if (isOwnerEntry) return;
2618
+ const cur = await beast.dcGetAllow();
2619
+ const next = cur.map((e, i) => {
2620
+ if (typeof e === 'object' && e && e.owner) return { ...e, owner: false };
2621
+ if (i === idx) {
2622
+ const obj = typeof e === 'string' ? { id: e, name: '' } : { ...e };
2623
+ obj.owner = true;
2624
+ return obj;
2625
+ }
2626
+ return e;
2627
+ });
2628
+ const tgt = next[idx];
2629
+ await beast.dcSetAllow(next);
2630
+ renderDcAllow();
2631
+ toast('Sahip: ' + dcLabel(tgt));
2632
+ });
2633
+ c.appendChild(ownerBtn);
2634
+ }
2635
+ const x = document.createElement('span');
2636
+ x.className = 'x';
2637
+ x.textContent = '×';
2638
+ x.addEventListener('click', async () => {
2639
+ const next = (await beast.dcGetAllow()).filter((_v, i) => i !== idx);
2640
+ await beast.dcSetAllow(next);
2641
+ renderDcAllow();
2642
+ toast('Kaldırıldı: ' + dcLabel(entry));
2643
+ });
2644
+ c.appendChild(x);
2645
+ wrap.appendChild(c);
2646
+ });
2647
+
2648
+ const inp = $('#dcAllowIdInp');
2649
+ const nameInp = $('#dcAllowNameInp');
2650
+ const add = $('#dcAllowAdd');
2651
+ const botSel = $('#dcAllowBotSel');
2652
+ if (botSel) {
2653
+ const prev = botSel.value;
2654
+ botSel.innerHTML =
2655
+ `<option value="">${_t('bot_sel_empty')}</option>` +
2656
+ botChoices
2657
+ .map((bb) => `<option value="${bb.id}">${bb.icon} ${escapeHtml(bb.name)}${bb.admin ? ' (admin)' : ''}</option>`)
2658
+ .join('');
2659
+ if (prev) botSel.value = prev;
2660
+ }
2661
+ if (!add.dataset.bound) {
2662
+ add.dataset.bound = '1';
2663
+ const addEntry = async () => {
2664
+ let v = inp.value.trim();
2665
+ if (!v) return;
2666
+ const name = nameInp.value.trim().slice(0, 40);
2667
+ if (!name) { toast('İsim zorunlu — kimin yazdığını bilmek için'); nameInp.focus(); return; }
2668
+ v = v.startsWith('@') ? '@' + v.slice(1).replace(/[^\w]/g, '') : v.replace(/[^\d]/g, '');
2669
+ if (!v) { toast(_t('it_dc_id_ph')); return; }
2670
+ const botId = botSel && botSel.value ? { bot_id: botSel.value } : {};
2671
+ const next = [...(await beast.dcGetAllow()), { id: v, name, ...botId }];
2672
+ if (!next.some((e) => e && typeof e === 'object' && e.owner)) {
2673
+ if (next.length && typeof next[0] === 'object') next[0].owner = true;
2674
+ }
2675
+ await beast.dcSetAllow(next);
2676
+ inp.value = '';
2677
+ nameInp.value = '';
2678
+ renderDcAllow();
2679
+ toast('Eklendi: ' + name);
2680
+ };
2681
+ add.addEventListener('click', addEntry);
2682
+ inp.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); addEntry(); } });
2683
+ nameInp.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); addEntry(); } });
2684
+ }
2685
+ }
2686
+
2442
2687
  /* ---------------- BOT SİSTEMİ (BÖLÜM 2-3-4) ----------------
2443
2688
  Sol panel altında bot kartları; her bot için sohbet geçmişi + sekmeli yönetim.
2444
2689
  İlk bot hep Beast (admin, silinemez). Max 5 bot. */
@@ -2767,6 +3012,15 @@ function renderBotSettings(pane, b) {
2767
3012
  const numRows = (b.numbers || [])
2768
3013
  .map((n) => `<div class="bot-num-row" data-num="${n.num}"><span class="n">+${escapeHtml(n.num)}${n.name ? ' · ' + escapeHtml(n.name) : ''}</span><span class="x" title="${_t('bot_num_del')}">×</span></div>`)
2769
3014
  .join('');
3015
+ /* bot bazlı model seçenekleri: ana picker'daki listeyle aynı (state.models) */
3016
+ const modelOpts = ['<option value="">' + escapeHtml(_t('bot_model_global')) + '</option>']
3017
+ .concat(
3018
+ (state.models || []).map((m) => {
3019
+ const s = m.sel || (m.providerId || '') + '::' + (m.model || '');
3020
+ return `<option value="${escapeHtml(s)}" ${b.model === s ? 'selected' : ''}>${escapeHtml(m.providerName || m.providerId || '')} · ${escapeHtml(m.model || '')}</option>`;
3021
+ })
3022
+ )
3023
+ .join('');
2770
3024
  /* Numara ekleme KALDIRILDI — girişler yalnız Ayarlar → Entegrasyonlar (WhatsApp
2771
3025
  izin listesi) üzerinden yapılır. Bu bölüm salt-okunur bağlı-numara listesidir. */
2772
3026
  f.innerHTML = `
@@ -2789,7 +3043,10 @@ function renderBotSettings(pane, b) {
2789
3043
  <div class="bot-checks" id="bPerm">${permChecks}</div>
2790
3044
  <div class="sub" style="margin-top:4px">${_t('bot_perm_note')}</div>
2791
3045
  <label class="mem-label">${_t('bot_skills')}</label>
2792
- <div class="bot-checks" id="bSkills">${skillChecks}</div>`}
3046
+ <div class="bot-checks" id="bSkills">${skillChecks}</div>
3047
+ <label class="mem-label">${_t('bot_model')}</label>
3048
+ <select id="bModel" class="inp">${modelOpts}</select>
3049
+ <div class="sub" style="margin-top:4px">${_t('bot_model_note')}</div>`}
2793
3050
  <label class="mem-label">${_t('bot_browser')}</label>
2794
3051
  <div class="bot-checks" style="margin-bottom:6px">
2795
3052
  <label><input type="checkbox" id="bExtBrowser" ${b.extBrowser ? 'checked' : ''}/> ${_t('bot_ext_browser')}</label>
@@ -2864,6 +3121,8 @@ function renderBotSettings(pane, b) {
2864
3121
  const sk = {};
2865
3122
  f.querySelectorAll('#bSkills input').forEach((c) => { sk[c.dataset.skill] = c.checked; });
2866
3123
  patch.skills = sk;
3124
+ const bm = $('#bModel');
3125
+ if (bm) patch.model = bm.value;
2867
3126
  }
2868
3127
  const r = await beast.botsUpdate(b.id, patch);
2869
3128
  if (r.ok) { toast(_t('bot_saved')); await refreshBots(); renderBotPage(); }
@@ -3087,6 +3346,39 @@ function fmtAgo(iso) {
3087
3346
  return s >= 60 ? Math.floor(s / 60) + 'dk' : s + 'sn';
3088
3347
  }
3089
3348
 
3349
+ /* canlı süre sayacı: 0:43 · 2:05 · 1:02:15 biçimi (koşan ajan kartlarında) */
3350
+ function fmtElapsed(ms) {
3351
+ const s = Math.max(0, Math.floor(ms / 1000));
3352
+ const h = Math.floor(s / 3600);
3353
+ const m = Math.floor((s % 3600) / 60);
3354
+ const sec = s % 60;
3355
+ const p = (n) => String(n).padStart(2, '0');
3356
+ return h > 0 ? `${h}:${p(m)}:${p(sec)}` : `${m}:${p(sec)}`;
3357
+ }
3358
+
3359
+ /* koşan ajanların ⏱ sayaçları — saniyede bir yalnızca sayaç span'leri güncellenir
3360
+ (tüm rail yeniden çizilmez). Koşan kart kalmayınca interval kapanır. */
3361
+ let agentTimerInt = null;
3362
+ function ensureAgentTimers() {
3363
+ const any = !!document.querySelector('.agent-timer[data-start]');
3364
+ if (any && !agentTimerInt) {
3365
+ agentTimerInt = setInterval(() => {
3366
+ document.querySelectorAll('.agent-timer[data-start]').forEach((el) => {
3367
+ const t0 = Date.parse(el.dataset.start || '');
3368
+ if (t0) el.textContent = fmtElapsed(Date.now() - t0);
3369
+ });
3370
+ }, 1000);
3371
+ } else if (!any && agentTimerInt) {
3372
+ clearInterval(agentTimerInt);
3373
+ agentTimerInt = null;
3374
+ }
3375
+ }
3376
+ function agentTimerHtml(startedAt) {
3377
+ const t0 = Date.parse(startedAt || '');
3378
+ const val = t0 ? fmtElapsed(Date.now() - t0) : '0:00';
3379
+ return `<span class="agent-timer" data-start="${escapeHtml(startedAt || '')}">⏱ ${val}</span>`;
3380
+ }
3381
+
3090
3382
  /* sağ panel — paralel ajanların arka plan işleri */
3091
3383
  function renderAgentRail() {
3092
3384
  const list = els.railList;
@@ -3117,7 +3409,7 @@ function renderAgentRail() {
3117
3409
  `<span class="ag-dot"></span>` +
3118
3410
  `<span class="sess-title">${escapeHtml(j.title)}</span>` +
3119
3411
  (j.code ? `<span class="sess-code" title="Oturum kodu">${escapeHtml(j.code)}</span>` : ``) +
3120
- `<span class="rj-time">${j.status === 'running' ? when + ' · ' + _t('ag_working') : (agStText(j.status) === _t('ag_st_done') ? '\u2713' : (agStText(j.status) || j.status))}</span>` +
3412
+ `<span class="rj-time">${j.status === 'running' ? when + ' · ' + agentTimerHtml(j.startedAt) + ' · ' + _t('ag_working') : (agStText(j.status) === _t('ag_st_done') ? '\u2713' : (agStText(j.status) || j.status))}</span>` +
3121
3413
  (j.status === 'running'
3122
3414
  ? `<button class="rj-cancel" title="${_t('ag_cancel')}">×</button>`
3123
3415
  : `<button class="rj-cancel" title="${_t('ag_delete')}"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M10 11v6M14 11v6"/></svg></button>`) +
@@ -3162,6 +3454,7 @@ function renderAgentRail() {
3162
3454
  /* açık sohbet dökümünü re-render sonrası GERİ GETİR (kapanma bug'ının fix'i) */
3163
3455
  if (open && agentState.chatOpen.has(j.id)) maybeLoadRailChat(j.id);
3164
3456
  }
3457
+ ensureAgentTimers();
3165
3458
  }
3166
3459
 
3167
3460
  /* re-render storm'da IPC fırtınası olmasın: döküm 2 sn'de bir tazelenir */
@@ -3247,7 +3540,7 @@ function renderAgentsPane() {
3247
3540
  `<span class="ag-dot"></span>` +
3248
3541
  `<span class="ag-title">${escapeHtml(j.title)}</span>` +
3249
3542
  `<span class="ag-st">${agStText(j.status) || j.status}</span>` +
3250
- `<span class="ag-time">${when}${j.endedAt ? ` · ${fmtAgo(j.endedAt)}` : ''}</span>` +
3543
+ `<span class="ag-time">${when}${j.status === 'running' ? ' · ' + agentTimerHtml(j.startedAt) : j.endedAt ? ` · ${fmtAgo(j.endedAt)}` : ''}</span>` +
3251
3544
  (j.status === 'running'
3252
3545
  ? `<button class="ag-btn cancel" title="${_t('ag_cancel')}">${_t('ag_cancel')}</button>`
3253
3546
  : ``) +
@@ -3285,16 +3578,8 @@ function renderAgentsPane() {
3285
3578
  list.appendChild(card);
3286
3579
  }
3287
3580
 
3288
- /* canlı süre sayacı */
3289
- clearInterval(agentState.tick);
3290
- agentState.tick = setInterval(() => {
3291
- if (setTab !== 'agents') { clearInterval(agentState.tick); return; }
3292
- for (const el of document.querySelectorAll('.agent-card.st-running .ag-time')) {
3293
- /* sadece saati yenile */
3294
- const t = el.textContent.split(' ')[0];
3295
- el.textContent = `${t} · ${_t('ag_working')}…`;
3296
- }
3297
- }, 5000);
3581
+ /* canlı süre sayacı — saniyede bir yalnız ⏱ sayaç span'leri güncellenir */
3582
+ ensureAgentTimers();
3298
3583
  }
3299
3584
 
3300
3585
  /* ---------------- events from engine ---------------- */
@@ -4262,6 +4547,7 @@ async function init() {
4262
4547
 
4263
4548
  beast.onWaEvent(onWaEvent);
4264
4549
  beast.onTgEvent(onTgEvent);
4550
+ beast.onDcEvent(onDcEvent);
4265
4551
 
4266
4552
  els.gearBtn.addEventListener('click', openSettings);
4267
4553
  els.setClose.addEventListener('click', closeSettings);
@@ -121,18 +121,14 @@ function fakeJob(over) {
121
121
  };
122
122
  }
123
123
 
124
- test('superviseReason: takılan stuck, çalışan uzun iş once wrapup (wrapAt sonrası long), yeni uyarılan null', () => {
124
+ test('superviseReason: takılan stuck, uzun koşan iş CEO kontrolü (long), yeni uyarılan null', () => {
125
125
  assert.strictEqual(Engine.superviseReason(fakeJob(), Date.now()), 'stuck');
126
126
 
127
+ // süre sınırı KALDIRILDI: aktivite süren uzun iş artık kesilmez → CEO ara kontrolü (long)
127
128
  assert.strictEqual(
128
129
  Engine.superviseReason(fakeJob({ lastActivityAt: new Date(Date.now() - 30 * 1000).toISOString() }), Date.now()),
129
- 'wrapup'
130
- ); // aktivite sürüyor ama 10 dk koştu — önce zarif bitirme uyarısı
131
-
132
- assert.strictEqual(
133
- Engine.superviseReason(fakeJob({ wrapAt: new Date().toISOString(), lastActivityAt: new Date(Date.now() - 30 * 1000).toISOString() }), Date.now()),
134
130
  'long'
135
- ); // wrap-up uyarısı verildi, hâlâ bitmedi → CEO ara kontrolü
131
+ );
136
132
 
137
133
  assert.strictEqual(
138
134
  Engine.superviseReason(fakeJob({ startedAt: new Date(Date.now() - 1 * MIN).toISOString() }), Date.now()),
@@ -182,12 +178,11 @@ test('_supervise önce ajana ÖZ-KURTARMA verir, haklar bitince CEO\'ya uyarır'
182
178
  eng._supervise();
183
179
  assert.strictEqual(job.checks, 1);
184
180
 
185
- /* 2. aşama: öz-kurtarma hakları bitmiş + iş hâlâ koşuyor (wrap-up zaten
186
- verildi, aktif) → CEO uyarısı ('long') */
181
+ /* 2. aşama: öz-kurtarma hakları bitmiş + iş hâlâ koşuyor (aktivite sürüyor,
182
+ süre sınırı yok) → CEO uyarısı ('long') */
187
183
  job.status = 'running';
188
184
  job.endedAt = null;
189
185
  job.fixes = 2;
190
- job.wrapAt = new Date().toISOString();
191
186
  job.lastActivityAt = new Date(Date.now() - 30 * 1000).toISOString();
192
187
  job.lastNudgeAt = new Date(Date.now() - 5 * MIN).toISOString();
193
188
  eng._supervise();
package/tests/llm.test.js CHANGED
@@ -3,7 +3,7 @@
3
3
  require('./setup');
4
4
  const test = require('node:test');
5
5
  const assert = require('node:assert');
6
- const { withRetries, friendlyError } = require('../src/agent/llm');
6
+ const { withRetries, friendlyError, sumUsage, chatStreamAuto } = require('../src/agent/llm');
7
7
 
8
8
  test('withRetries: 503 sonrası başarılı isteği tekrarlar', async () => {
9
9
  let calls = 0;
@@ -54,3 +54,83 @@ test('friendlyError ipuçları içerir', () => {
54
54
  assert.match(m, /kullanılamıyor/);
55
55
  assert.match(m, /model seçici/);
56
56
  });
57
+
58
+ test('sumUsage: sayısal alanları toplar (devam turu faturalaması)', () => {
59
+ const u = sumUsage(
60
+ { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
61
+ { prompt_tokens: 8, completion_tokens: 3, total_tokens: 11 }
62
+ );
63
+ assert.deepEqual(u, { prompt_tokens: 18, completion_tokens: 8, total_tokens: 26 });
64
+ assert.equal(sumUsage(null, { total_tokens: 3 }).total_tokens, 3);
65
+ assert.equal(sumUsage({ total_tokens: 7 }, null).total_tokens, 7);
66
+ });
67
+
68
+ /* SSE mock sunucusu: finish_reason=length → otomatik devam turu açılır */
69
+ function sseServer(script) {
70
+ const http = require('node:http');
71
+ const calls = [];
72
+ const srv = http.createServer((req, res) => {
73
+ let body = '';
74
+ req.on('data', (c) => (body += c));
75
+ req.on('end', () => {
76
+ calls.push(JSON.parse(body).messages);
77
+ res.writeHead(200, { 'Content-Type': 'text/event-stream' });
78
+ for (const ev of script(calls.length, calls[calls.length - 1])) res.write(ev);
79
+ res.end();
80
+ });
81
+ });
82
+ return { srv, calls };
83
+ }
84
+
85
+ test('chatStreamAuto: length kesintisinde kaldığı yerden devam eder', async () => {
86
+ const { srv, calls } = sseServer((n) => {
87
+ if (n === 1) {
88
+ return [
89
+ 'data: ' + JSON.stringify({ choices: [{ delta: { content: 'Merhaba dun' } }] }) + '\n\n',
90
+ 'data: ' + JSON.stringify({ choices: [{ delta: { content: 'ya basladi' }, finish_reason: 'length' }] }) + '\n\n',
91
+ 'data: [DONE]\n\n',
92
+ ];
93
+ }
94
+ return [
95
+ 'data: ' + JSON.stringify({ choices: [{ delta: { content: 'yor devam' } }] }) + '\n\n',
96
+ 'data: ' + JSON.stringify({ choices: [{ delta: {}, finish_reason: 'stop' }] }) + '\n\n',
97
+ 'data: [DONE]\n\n',
98
+ ];
99
+ });
100
+ await new Promise((r) => srv.listen(0, '127.0.0.1', r));
101
+ try {
102
+ const port = srv.address().port;
103
+ const sel = { url: `http://127.0.0.1:${port}/v1/chat/completions`, key: 'k', model: 'm' };
104
+ const deltas = [];
105
+ const r = await chatStreamAuto(sel, { messages: [{ role: 'user', content: 'selam' }] }, { onDelta: (d) => deltas.push(d) });
106
+ assert.equal(r.content, 'Merhaba dunya basladiyor devam');
107
+ assert.equal(r.finishReason, 'stop');
108
+ assert.equal(calls.length, 2);
109
+ // 2. turda devam mesajları eklenmiş olmalı (orijinal dizi kopyalanır)
110
+ const m2 = calls[1];
111
+ assert.equal(m2[m2.length - 2].role, 'assistant');
112
+ assert.equal(m2[m2.length - 1].role, 'user');
113
+ assert.match(m2[m2.length - 1].content, /DEVAM/);
114
+ // streaming kesintisiz: 3 delta
115
+ assert.deepEqual(deltas.join(''), r.content);
116
+ } finally {
117
+ srv.close();
118
+ }
119
+ });
120
+
121
+ test('chatStreamAuto: normal (stop) yanıtta hiç devam turu açılmaz', async () => {
122
+ const { srv, calls } = sseServer(() => [
123
+ 'data: ' + JSON.stringify({ choices: [{ delta: { content: 'tam cevap' }, finish_reason: 'stop' }] }) + '\n\n',
124
+ 'data: [DONE]\n\n',
125
+ ]);
126
+ await new Promise((r) => srv.listen(0, '127.0.0.1', r));
127
+ try {
128
+ const port = srv.address().port;
129
+ const sel = { url: `http://127.0.0.1:${port}/v1/chat/completions`, key: 'k', model: 'm' };
130
+ const r = await chatStreamAuto(sel, { messages: [{ role: 'user', content: 'selam' }] });
131
+ assert.equal(r.content, 'tam cevap');
132
+ assert.equal(calls.length, 1);
133
+ } finally {
134
+ srv.close();
135
+ }
136
+ });
@@ -48,8 +48,9 @@ test('paketlenmiş python scriptleri mevcut ve tohumlanır', () => {
48
48
  /* canlı: python hızlı arama yolu (ağ varsa gerçek sonuç, yoksa JS fallback) */
49
49
  test('webSearchFast: python yolu ya da fallback sonuç döner', async function () {
50
50
  const r = await tools.webSearchFast('electron builder portable windows', { maxResults: 5 });
51
- assert.strictEqual(r.ok, true);
52
- if (!r.results.length) return this.skip('motorlar bu turda sonuç döndürmedi (ağ/rate-limit) — kablaj doğru');
51
+ /* rate-limit/CAPTCHA turlarında motorlar ok:false dönebilir — kablaj testi;
52
+ ağ nazik davranınca sonuç şekli doğrulanır, aksi halde skip */
53
+ if (!r || !r.ok || !r.results.length) return this.skip('motorlar bu turda sonuç döndürmedi (ağ/rate-limit) — kablaj doğru');
53
54
  assert.ok(r.results[0].title && r.results[0].url);
54
55
  }, { timeout: 40000 });
55
56