beast-agent 2.8.2 → 2.9.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 +117 -7
- package/src/main.js +53 -20
- package/src/renderer/renderer.js +65 -13
package/package.json
CHANGED
package/src/agent/engine.js
CHANGED
|
@@ -334,6 +334,9 @@ class Engine {
|
|
|
334
334
|
this._bgGroups = new Map(); // groupId -> {parentId,total,results,dead}
|
|
335
335
|
/* silinen oturum işaretleri: rapor/zincir buraya gönderilmez (hayalet oturum yok) */
|
|
336
336
|
this._deletedSessions = new Set();
|
|
337
|
+
/* OPENCODE STEER: koşan tur sırasında gelen mesajlar — güvenli floş
|
|
338
|
+
noktasına kadar burada bekler (sid -> msg[]) */
|
|
339
|
+
this._steerBuf = new Map();
|
|
337
340
|
/* #17 kalıcı ajan geçmişi: restart sonrası işler + sohbetler kaybolmasın */
|
|
338
341
|
this._bgJobsFile = path.join(this.sessionsDir, 'bg-jobs.json');
|
|
339
342
|
this._loadBgJobsPersisted();
|
|
@@ -1405,6 +1408,7 @@ class Engine {
|
|
|
1405
1408
|
if (cached && cached.code) this._codeIndex.delete(cached.code);
|
|
1406
1409
|
this.cache.delete(String(id));
|
|
1407
1410
|
this.todos.delete(String(id));
|
|
1411
|
+
this._steerBuf && this._steerBuf.delete(String(id));
|
|
1408
1412
|
this.sessionPerm.delete(String(id));
|
|
1409
1413
|
this.sessionTools.delete(String(id));
|
|
1410
1414
|
this._errCount && this._errCount.delete(String(id));
|
|
@@ -2009,7 +2013,7 @@ class Engine {
|
|
|
2009
2013
|
const small = this.modelFor('subagent') || sel || this.sel;
|
|
2010
2014
|
const res = await chatOnce(
|
|
2011
2015
|
small,
|
|
2012
|
-
{ messages: [{ role: 'user', content: prompt }], temperature: 0.2 },
|
|
2016
|
+
{ messages: [{ role: 'user', content: prompt }], temperature: 0.2, cacheKey: String(session.id || '') },
|
|
2013
2017
|
{ signal }
|
|
2014
2018
|
);
|
|
2015
2019
|
const text = String(res.content || '').replace(/```(?:markdown)?/gi, '').trim();
|
|
@@ -2172,7 +2176,7 @@ class Engine {
|
|
|
2172
2176
|
`# SOHBET PARÇASI\n${transcript}`;
|
|
2173
2177
|
const res = await chatOnce(
|
|
2174
2178
|
this.sel,
|
|
2175
|
-
{ messages: [{ role: 'user', content: prompt }], temperature: 0.2 },
|
|
2179
|
+
{ messages: [{ role: 'user', content: prompt }], temperature: 0.2, cacheKey: String(session.id || '') },
|
|
2176
2180
|
{ signal }
|
|
2177
2181
|
);
|
|
2178
2182
|
const txt = String(res.content || '').trim();
|
|
@@ -2231,7 +2235,7 @@ class Engine {
|
|
|
2231
2235
|
if (this._stopped && !userAction) return false;
|
|
2232
2236
|
if (userAction) this._stopped = false;
|
|
2233
2237
|
const s = this._load(String(sessionId));
|
|
2234
|
-
if (!s
|
|
2238
|
+
if (!s) return false;
|
|
2235
2239
|
|
|
2236
2240
|
let msg;
|
|
2237
2241
|
if (typeof payload === 'string') {
|
|
@@ -2262,6 +2266,11 @@ class Engine {
|
|
|
2262
2266
|
...imgs.map((im) => ({ type: 'image', name: String(im.name || 'resim') })),
|
|
2263
2267
|
...files,
|
|
2264
2268
|
];
|
|
2269
|
+
/* MEDYA KAYDI: WA/Chat UI'dan gelen fotoğraflar ve belgeler OTURUMUN
|
|
2270
|
+
media klasörüne yazılır (orijinal dosya güvende). msg.media jsonl'e
|
|
2271
|
+
düşer; prune/compaction görselleri metne indirse bile kayıp yok. */
|
|
2272
|
+
const media = this._saveMedia(s.id, imgs, files);
|
|
2273
|
+
if (media) msg.media = media;
|
|
2265
2274
|
}
|
|
2266
2275
|
}
|
|
2267
2276
|
|
|
@@ -2274,6 +2283,35 @@ class Engine {
|
|
|
2274
2283
|
/* Önceki tur yanıtlanmadan kaldıysa (hata/durdurma sonrası artık user mesajı)
|
|
2275
2284
|
katı sağlayıcılar art arda user mesajını reddeder: "messages illegal" (400).
|
|
2276
2285
|
Yeni metni o mesajla BİRLEŞTİR — dosyadaki son satır da güncellenir. */
|
|
2286
|
+
/* OPENCODE STEER PORTU (runtime.queue.ts + prompt.ts runLoop): oturum
|
|
2287
|
+
KOŞARKEN gelen mesaj hemen geçmişe yazılmaz — _steerBuf'ta bekler,
|
|
2288
|
+
koşan tur GÜVENLİ floş noktasında (tur başı / çıkış kontrolü) ekler.
|
|
2289
|
+
Böylece araç sonuçlarının arasına sıkışmaz, isteğin yarısındaki mesaj
|
|
2290
|
+
mutasyona uğramaz. UI'ya yine de ANINDA düşer. */
|
|
2291
|
+
if (this.ctrls.has(s.id)) {
|
|
2292
|
+
let buf = this._steerBuf.get(s.id);
|
|
2293
|
+
if (!buf) {
|
|
2294
|
+
buf = [];
|
|
2295
|
+
this._steerBuf.set(s.id, buf);
|
|
2296
|
+
}
|
|
2297
|
+
const lastBuf = buf[buf.length - 1];
|
|
2298
|
+
if (
|
|
2299
|
+
lastBuf && lastBuf.role === 'user' &&
|
|
2300
|
+
!Array.isArray(lastBuf.content) && !Array.isArray(msg.content) &&
|
|
2301
|
+
typeof msg.content === 'string'
|
|
2302
|
+
) {
|
|
2303
|
+
/* ardışık steer'ler tek mesajda birleşir (art arda user reddi olmasın) */
|
|
2304
|
+
lastBuf.content = lastBuf.content + '\n' + msg.content;
|
|
2305
|
+
if (msg.attachments || lastBuf.attachments) {
|
|
2306
|
+
lastBuf.attachments = [...(lastBuf.attachments || []), ...(msg.attachments || [])];
|
|
2307
|
+
}
|
|
2308
|
+
} else {
|
|
2309
|
+
buf.push(msg);
|
|
2310
|
+
}
|
|
2311
|
+
this.emit({ type: 'message', sessionId: s.id, message: msg });
|
|
2312
|
+
this.emit({ type: 'sessions' });
|
|
2313
|
+
return true;
|
|
2314
|
+
}
|
|
2277
2315
|
const lastMsg = s.messages[s.messages.length - 1];
|
|
2278
2316
|
if (
|
|
2279
2317
|
lastMsg && lastMsg.role === 'user' &&
|
|
@@ -2298,6 +2336,57 @@ class Engine {
|
|
|
2298
2336
|
return true;
|
|
2299
2337
|
}
|
|
2300
2338
|
|
|
2339
|
+
/* Steer tamponunu güvenli noktada geçmişe akıtır. Dönüş: bir şey akıtıldı mı. */
|
|
2340
|
+
_flushSteer(sid, session, emit) {
|
|
2341
|
+
const buf = this._steerBuf.get(sid);
|
|
2342
|
+
if (!buf || !buf.length) return false;
|
|
2343
|
+
this._steerBuf.delete(sid);
|
|
2344
|
+
for (const m of buf) {
|
|
2345
|
+
session.messages.push(m);
|
|
2346
|
+
try {
|
|
2347
|
+
this._append(session, m);
|
|
2348
|
+
} catch {}
|
|
2349
|
+
}
|
|
2350
|
+
if (emit) emit({ type: 'sessions' });
|
|
2351
|
+
return true;
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
/* MEDYA KAYDI: mesaj eklerindeki fotoğraflar (dataUrl) ve metin belgeleri
|
|
2355
|
+
oturumun media klasörüne yazılır: <sessionsDir>/media/<sid>/zaman-metin.
|
|
2356
|
+
Dönen liste msg.media olarak jsonl'e yazılır — orijinal dosya hiç
|
|
2357
|
+
kaybolmaz (eski görsellerin metne indirilmesinden bağımsız). */
|
|
2358
|
+
_saveMedia(sid, imgs, files) {
|
|
2359
|
+
const out = [];
|
|
2360
|
+
try {
|
|
2361
|
+
const dir = path.join(this.sessionsDir, 'media', String(sid || ''));
|
|
2362
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2363
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
2364
|
+
const safe = (n, fb) => String(n || fb).replace(/[\\/:*?"<>|]/g, '_').slice(0, 80);
|
|
2365
|
+
const stripExt = (n) => String(n || '').replace(/\.[a-z0-9]{1,5}$/i, '');
|
|
2366
|
+
let i = 0;
|
|
2367
|
+
for (const im of imgs) {
|
|
2368
|
+
const m = /^data:([^;,]+)?(;base64)?,(.*)$/.exec(String(im.dataUrl || ''));
|
|
2369
|
+
if (!m) continue;
|
|
2370
|
+
try {
|
|
2371
|
+
const mime = String(m[1] || 'image/png');
|
|
2372
|
+
const ext = (mime.split('/')[1] || 'png').replace(/[^a-z0-9]/gi, '').slice(0, 5) || 'png';
|
|
2373
|
+
const buf = m[2] ? Buffer.from(m[3], 'base64') : Buffer.from(decodeURIComponent(m[3]), 'utf8');
|
|
2374
|
+
const file = stamp + '-' + ++i + '-' + safe(stripExt(im.name), 'resim') + '.' + ext;
|
|
2375
|
+
fs.writeFileSync(path.join(dir, file), buf);
|
|
2376
|
+
out.push({ file, name: String(im.name || 'resim'), kind: 'image' });
|
|
2377
|
+
} catch {}
|
|
2378
|
+
}
|
|
2379
|
+
for (const f of files) {
|
|
2380
|
+
try {
|
|
2381
|
+
const file = stamp + '-' + ++i + '-' + safe(stripExt(f.name), 'dosya') + '.txt';
|
|
2382
|
+
fs.writeFileSync(path.join(dir, file), String(f.content || ''), 'utf8');
|
|
2383
|
+
out.push({ file, name: String(f.name || 'dosya'), kind: 'file' });
|
|
2384
|
+
} catch {}
|
|
2385
|
+
}
|
|
2386
|
+
} catch {}
|
|
2387
|
+
return out.length ? out : null;
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2301
2390
|
/* SESSİZ BAĞLAM ENJEKSİYONU: metin oturum geçmişine düşer ama _run()
|
|
2302
2391
|
TETİKLENMEZ — ajan "görür", cevap ÜRETMEZ. WhatsApp gruplarında
|
|
2303
2392
|
@mention moduyla kullanılır: grubun tüm konuşması bağlam olarak
|
|
@@ -3249,6 +3338,10 @@ class Engine {
|
|
|
3249
3338
|
e.name = 'AbortError';
|
|
3250
3339
|
throw e;
|
|
3251
3340
|
}
|
|
3341
|
+
/* STEER FLOŞU (tur başı): koşan tur sırasında gelen mesajlar burada
|
|
3342
|
+
geçmişe eklenir — araç sonuçlarının arasına sıkışmaz, sonraki
|
|
3343
|
+
_chatTurn isteğinde model görür */
|
|
3344
|
+
this._flushSteer(sid, session, emit);
|
|
3252
3345
|
/* SÜRE SINIRI YOK. Ana oturum sınırsız tur koşar (opencode birebir);
|
|
3253
3346
|
yalnız bg ajan son 3 tura gelirken "raporu yaz ve bitir" uyarısı alır. */
|
|
3254
3347
|
if (session.bgJob && turn === maxTurns - 3 && !wrapNudged) {
|
|
@@ -3278,6 +3371,9 @@ class Engine {
|
|
|
3278
3371
|
}
|
|
3279
3372
|
this._bgTrim(session); // #21 bg geçmişini olabildiğince ince tut
|
|
3280
3373
|
emit({ type: 'status', status: 'thinking' });
|
|
3374
|
+
/* opencode runLoop portu: istek kurulmadan ÖNCEKİ mesaj sayısı —
|
|
3375
|
+
koşan tur SIRASINDA gelen (steer) user mesajlarını saptar */
|
|
3376
|
+
const preLen = session.messages.length;
|
|
3281
3377
|
const res = await this._chatTurn(session, ctrl.signal, (delta) =>
|
|
3282
3378
|
emit({ type: 'token', delta })
|
|
3283
3379
|
);
|
|
@@ -3298,6 +3394,16 @@ class Engine {
|
|
|
3298
3394
|
emit({ type: 'message', message: assistant });
|
|
3299
3395
|
|
|
3300
3396
|
if (!res.toolCalls || !res.toolCalls.length) {
|
|
3397
|
+
/* opencode runLoop portu (prompt.ts:1111-1115): koşan tur SIRASINDA
|
|
3398
|
+
yeni user mesajı geldiyse (steer) tur BİTMEZ — tampondaki mesaj
|
|
3399
|
+
geçmişe akıtılır, model sonraki istekte görür ve cevaplar;
|
|
3400
|
+
eski cevap akışı bozulmaz. */
|
|
3401
|
+
if (
|
|
3402
|
+
session.messages.slice(preLen).some((m) => m.role === 'user') ||
|
|
3403
|
+
this._flushSteer(sid, session, emit)
|
|
3404
|
+
) {
|
|
3405
|
+
continue;
|
|
3406
|
+
}
|
|
3301
3407
|
/* GÖREV LİSTESİ DİSİPLİNİ: ajan işi bitti sanıyor ama listede hâlâ
|
|
3302
3408
|
bekleyen/aktif madde varsa BİR KEZ hatırlat ve tura devam et —
|
|
3303
3409
|
liste ya tamamlanmalı ya kalan maddeler listeden düşürülmeli. */
|
|
@@ -3458,6 +3564,10 @@ class Engine {
|
|
|
3458
3564
|
this._bgFinish(sid, 'error', 'bg görevi tur limitine ulaştı (maxTurns)');
|
|
3459
3565
|
} catch (e) {
|
|
3460
3566
|
const aborted = e && (e.name === 'AbortError' || ctrl.signal.aborted);
|
|
3567
|
+
/* STEER FLOŞU (hata/durdurma): tur sırasında gelen mesajlar yine de
|
|
3568
|
+
geçmişe yazılsın — konuşma bütünlüğü korunur, sonraki tur bağlamı
|
|
3569
|
+
görür (opencode interrupted-part disiplini) */
|
|
3570
|
+
this._flushSteer(sid, session, emit);
|
|
3461
3571
|
if (aborted) {
|
|
3462
3572
|
this._clearCrash(); // kullanıcı durdurdu — kurtarma yok
|
|
3463
3573
|
/* öz-kurtarma/superyorizon kick'i bekliyorsa bu tur BİTİŞ değildir —
|
|
@@ -3571,7 +3681,7 @@ class Engine {
|
|
|
3571
3681
|
const llm = async (sys, user) => {
|
|
3572
3682
|
const res = await chatOnce(
|
|
3573
3683
|
this.sel,
|
|
3574
|
-
{ messages: [{ role: 'user', content: sys + '\n\n' + user }], temperature: 0.1 },
|
|
3684
|
+
{ messages: [{ role: 'user', content: sys + '\n\n' + user }], temperature: 0.1, cacheKey: String(session.id || '') },
|
|
3575
3685
|
{ signal }
|
|
3576
3686
|
);
|
|
3577
3687
|
return String(res.content || '');
|
|
@@ -3607,7 +3717,7 @@ class Engine {
|
|
|
3607
3717
|
|
|
3608
3718
|
const res = await chatOnce(
|
|
3609
3719
|
this.sel,
|
|
3610
|
-
{ messages: [{ role: 'user', content: prompt }], temperature: 0.1 },
|
|
3720
|
+
{ messages: [{ role: 'user', content: prompt }], temperature: 0.1, cacheKey: String(session.id || '') },
|
|
3611
3721
|
{ signal }
|
|
3612
3722
|
);
|
|
3613
3723
|
let parsed = null;
|
|
@@ -3661,7 +3771,7 @@ const skills = require('./skills');
|
|
|
3661
3771
|
|
|
3662
3772
|
const res = await chatOnce(
|
|
3663
3773
|
this.sel,
|
|
3664
|
-
{ messages: [{ role: 'user', content: prompt }], temperature: 0.2 },
|
|
3774
|
+
{ messages: [{ role: 'user', content: prompt }], temperature: 0.2, cacheKey: String(session.id || '') },
|
|
3665
3775
|
{}
|
|
3666
3776
|
);
|
|
3667
3777
|
const draft = parseReflectionJson(res.content || '');
|
|
@@ -3745,7 +3855,7 @@ const skills = require('./skills');
|
|
|
3745
3855
|
try {
|
|
3746
3856
|
const res = await chatOnce(
|
|
3747
3857
|
this.sel,
|
|
3748
|
-
{ messages: [{ role: 'user', content: prompt }], temperature: 0.2 },
|
|
3858
|
+
{ messages: [{ role: 'user', content: prompt }], temperature: 0.2, cacheKey: 'nightref' },
|
|
3749
3859
|
{ signal: ctrl.signal }
|
|
3750
3860
|
);
|
|
3751
3861
|
return String(res.content || '');
|
package/src/main.js
CHANGED
|
@@ -642,7 +642,7 @@ async function transcribeCloud(provider, buf, mimetype, lang) {
|
|
|
642
642
|
if (lang === 'tr' || lang === 'en') form.append('language', lang);
|
|
643
643
|
const res = await fetch(base + '/audio/transcriptions', {
|
|
644
644
|
method: 'POST',
|
|
645
|
-
headers: { Authorization: 'Bearer ' + key },
|
|
645
|
+
headers: { Authorization: 'Bearer ' + key, 'x-opencode-session': OPENCODE_SESSION },
|
|
646
646
|
body: form,
|
|
647
647
|
});
|
|
648
648
|
if (!res.ok) throw new Error(provider + ' ' + res.status + ': ' + String(await res.text()).slice(0, 180));
|
|
@@ -954,7 +954,11 @@ async function synthesizeSpeech(text) {
|
|
|
954
954
|
const url = String(cfg.baseUrl).replace(/\/+$/, '') + '/audio/speech';
|
|
955
955
|
const res = await fetch(url, {
|
|
956
956
|
method: 'POST',
|
|
957
|
-
headers: {
|
|
957
|
+
headers: {
|
|
958
|
+
'Content-Type': 'application/json',
|
|
959
|
+
Authorization: 'Bearer ' + cfg.key,
|
|
960
|
+
'x-opencode-session': OPENCODE_SESSION,
|
|
961
|
+
},
|
|
958
962
|
body: JSON.stringify({
|
|
959
963
|
model: cfg.model || 'tts-1',
|
|
960
964
|
input: String(text).slice(0, 4000),
|
|
@@ -1726,6 +1730,23 @@ function sessionFileAlive(sid) {
|
|
|
1726
1730
|
|
|
1727
1731
|
/* kanalın yaşayan tek oturumunu döndürür; geçersizse eski bağlamdan devralır,
|
|
1728
1732
|
o da yoksa (ilk kullanım) yeni açar ve tüm jid'leri ona bağlar */
|
|
1733
|
+
/* WA'dan gelen belge orijinallerini oturumun media klasörüne yazar;
|
|
1734
|
+
döner: media/<sid>/<dosya> (relatif yol — ajan read_file ile açabilir) */
|
|
1735
|
+
function saveSessionMedia(sid, name, buf) {
|
|
1736
|
+
try {
|
|
1737
|
+
if (!buf || !buf.length) return null;
|
|
1738
|
+
const dir = path.join(engine.sessionsDir, 'media', String(sid || ''));
|
|
1739
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1740
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
1741
|
+
const safe = String(name || 'dosya').replace(/[\\/:*?"<>|]/g, '_').slice(0, 120);
|
|
1742
|
+
const file = stamp + '-' + safe;
|
|
1743
|
+
fs.writeFileSync(path.join(dir, file), buf);
|
|
1744
|
+
return 'media/' + String(sid || '') + '/' + file;
|
|
1745
|
+
} catch {
|
|
1746
|
+
return null;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1729
1750
|
function ensureWaSession(jid) {
|
|
1730
1751
|
if (!waSingleSid || !sessionFileAlive(waSingleSid)) {
|
|
1731
1752
|
let best = '';
|
|
@@ -2429,12 +2450,15 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
|
|
|
2429
2450
|
if (inst && inst.error) {
|
|
2430
2451
|
text += `\n[SKILL.md alındı ama kaydedilemedi]`;
|
|
2431
2452
|
} else {
|
|
2453
|
+
/* BELGE KAYDI: orijinal dosya oturum media klasörüne yazılır —
|
|
2454
|
+
model yalnız metnini görür, orijinale ajan read_file ile ulaşabilir */
|
|
2455
|
+
const savedDoc = saveSessionMedia(sid, md.name, md.buf);
|
|
2432
2456
|
const dt = documentToText(md);
|
|
2433
2457
|
if (dt && dt.trim()) {
|
|
2434
2458
|
attachments.push({ type: 'file', name: md.name, content: dt.slice(0, 20000) });
|
|
2435
|
-
text += `\n[belge alındı: ${md.name}]`;
|
|
2459
|
+
text += `\n[belge alındı: ${md.name}${savedDoc ? ' · kaydedildi: ' + savedDoc : ''}]`;
|
|
2436
2460
|
} else {
|
|
2437
|
-
text += `\n[belge alındı ama okunamadı: ${md.name}]`;
|
|
2461
|
+
text += `\n[belge alındı ama okunamadı: ${md.name}${savedDoc ? ' · kaydedildi: ' + savedDoc : ''}]`;
|
|
2438
2462
|
}
|
|
2439
2463
|
}
|
|
2440
2464
|
}
|
|
@@ -5149,6 +5173,14 @@ function queueDesktopMessage(sessionId, text) {
|
|
|
5149
5173
|
chatQueueOfflineAdd(sid, { text: t, attachments: hasAtts ? text.attachments : undefined });
|
|
5150
5174
|
return;
|
|
5151
5175
|
}
|
|
5176
|
+
/* OPENCODE STEER: koşan tur varken mesaj ANINDA konuşmaya eklenir —
|
|
5177
|
+
ajan sonraki istekte görür, eski cevap akışı bozulmaz. Kuyrukta beklemesine
|
|
5178
|
+
gerek yok; boşta debounce penceresi yine birleştirir. */
|
|
5179
|
+
if (engine.isBusy(sid)) {
|
|
5180
|
+
waLog(`desktop: oturum meşgul — mesaj steer olarak konuşmaya eklendi sid=${sid}`);
|
|
5181
|
+
engine.send(sid, hasAtts ? { text: t, attachments: text.attachments } : t, { userAction: true });
|
|
5182
|
+
return;
|
|
5183
|
+
}
|
|
5152
5184
|
let q = desktopQueue.get(sid);
|
|
5153
5185
|
if (!q) {
|
|
5154
5186
|
q = { timer: null, msgs: [] };
|
|
@@ -5156,16 +5188,9 @@ function queueDesktopMessage(sessionId, text) {
|
|
|
5156
5188
|
}
|
|
5157
5189
|
q.msgs.push({ text: t, attachments: hasAtts ? text.attachments : undefined });
|
|
5158
5190
|
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
clearTimeout(q.timer);
|
|
5163
|
-
q.timer = setTimeout(() => flushDesktop(sid).catch(() => {}), DESKTOP_DEBOUNCE_MS);
|
|
5164
|
-
} else {
|
|
5165
|
-
/* agent çalışıyor: pencere yok, iş bitince hepsi birlikte gider.
|
|
5166
|
-
Bekleyen paket olduğunda 'done' olayı flushDesktop'u çağırır. */
|
|
5167
|
-
waLog(`desktop: oturum meşgul, mesaj beklemede (${q.msgs.length}) sid=${sid}`);
|
|
5168
|
-
}
|
|
5191
|
+
/* agent boşta: kısa pencere — hızlı ikinci mesaj ilkine eklenir */
|
|
5192
|
+
clearTimeout(q.timer);
|
|
5193
|
+
q.timer = setTimeout(() => flushDesktop(sid).catch(() => {}), DESKTOP_DEBOUNCE_MS);
|
|
5169
5194
|
}
|
|
5170
5195
|
|
|
5171
5196
|
async function flushDesktop(sessionId) {
|
|
@@ -6205,19 +6230,27 @@ function cronEmit() {
|
|
|
6205
6230
|
const cronAnswerPending = new Map();
|
|
6206
6231
|
|
|
6207
6232
|
/* OTOMATİK YENİ SOHBET YOK: cron/izleyici/fallout tetiklendiğinde oturum
|
|
6208
|
-
seçimi — kayıtlı id
|
|
6209
|
-
|
|
6210
|
-
geçmişine "+ Yeni Sohbet" olmadan hayalet
|
|
6233
|
+
seçimi — kayıtlı id'nin DOSYASI hâlâ duruyorsa O, değilse EN GÜNCEL
|
|
6234
|
+
(meşgul olmayan) oturum kullanılır; hiç oturum yoksa (ilk kurulum) yeni
|
|
6235
|
+
açılır. Böylece soldaki sohbet geçmişine "+ Yeni Sohbet" olmadan hayalet
|
|
6236
|
+
sohbetler düşmez.
|
|
6237
|
+
DİKKAT: _load(id) silinmiş oturum için bile boş bir hayalet nesne
|
|
6238
|
+
üretip truthy döner — canlılık kontrolü MUTLAKA sessionFileAlive ile
|
|
6239
|
+
yapılır (ensureWa/Tg/DcSession ile aynı kural). Ayrıca meşgul oturuma
|
|
6240
|
+
send() mesajı DÜŞÜRÜR (false döner, cron cevabı kaybolur) — o yüzden
|
|
6241
|
+
meşgul oturumlar da atlanır. */
|
|
6211
6242
|
function reuseOrLatestSession(preferredId) {
|
|
6212
6243
|
const sid = String(preferredId || '');
|
|
6213
|
-
if (sid) {
|
|
6244
|
+
if (sid && sessionFileAlive(sid)) {
|
|
6214
6245
|
try {
|
|
6215
|
-
if (engine._load(sid)) return sid;
|
|
6246
|
+
if (engine._load(sid) && !engine.isBusy(sid)) return sid;
|
|
6216
6247
|
} catch {}
|
|
6217
6248
|
}
|
|
6218
6249
|
try {
|
|
6219
6250
|
const list = engine.listSessions(); // updatedAt'e göre yeni→eski sıralı
|
|
6220
|
-
|
|
6251
|
+
for (const v of list) {
|
|
6252
|
+
if (!engine.isBusy(v.id)) return String(v.id);
|
|
6253
|
+
}
|
|
6221
6254
|
} catch {}
|
|
6222
6255
|
return engine.createSession().id;
|
|
6223
6256
|
}
|
package/src/renderer/renderer.js
CHANGED
|
@@ -5402,9 +5402,25 @@ function opText(w) {
|
|
|
5402
5402
|
return `${sym} ${w.value ?? '?'}`;
|
|
5403
5403
|
}
|
|
5404
5404
|
|
|
5405
|
-
|
|
5405
|
+
/* kısa saat: bugünse "14:32", başka günse "07.09 14:32" */
|
|
5406
|
+
function fmtWhen(iso) {
|
|
5407
|
+
if (!iso) return '';
|
|
5408
|
+
const d = new Date(iso);
|
|
5409
|
+
if (isNaN(d.getTime())) return '';
|
|
5410
|
+
const t = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
5411
|
+
if (d.toDateString() === new Date().toDateString()) return t;
|
|
5412
|
+
return d.toLocaleDateString([], { day: '2-digit', month: '2-digit' }) + ' ' + t;
|
|
5413
|
+
}
|
|
5414
|
+
|
|
5415
|
+
let lastWatchKey = '';
|
|
5416
|
+
async function renderWatchersModal(force) {
|
|
5406
5417
|
let rows = [];
|
|
5407
5418
|
try { rows = (await beast.watchersList()) || []; } catch {}
|
|
5419
|
+
/* canlı yenilemede içerik değişmediyse DOM'a dokunma — buton tıklamaları
|
|
5420
|
+
yeniden kurulurken yutulmasın */
|
|
5421
|
+
const key = JSON.stringify(rows);
|
|
5422
|
+
if (!force && key === lastWatchKey) return;
|
|
5423
|
+
lastWatchKey = key;
|
|
5408
5424
|
els.watchList.innerHTML = '';
|
|
5409
5425
|
if (!rows.length) {
|
|
5410
5426
|
els.watchList.innerHTML = '<div class="mini-empty">' + _t('w_empty') + '</div>';
|
|
@@ -5443,9 +5459,13 @@ async function renderWatchersModal() {
|
|
|
5443
5459
|
}
|
|
5444
5460
|
}
|
|
5445
5461
|
|
|
5446
|
-
|
|
5462
|
+
let lastCronKey = '';
|
|
5463
|
+
async function renderCronModal(force) {
|
|
5447
5464
|
let jobs = [];
|
|
5448
5465
|
try { jobs = (await beast.cronList()) || []; } catch {}
|
|
5466
|
+
const key = JSON.stringify(jobs);
|
|
5467
|
+
if (!force && key === lastCronKey) return;
|
|
5468
|
+
lastCronKey = key;
|
|
5449
5469
|
els.cronModalList.innerHTML = '';
|
|
5450
5470
|
if (!jobs.length) {
|
|
5451
5471
|
els.cronModalList.innerHTML = '<div class="mini-empty">' + _t('cr_modal_empty') + '</div>';
|
|
@@ -5454,11 +5474,15 @@ async function renderCronModal() {
|
|
|
5454
5474
|
for (const j of jobs) {
|
|
5455
5475
|
const row = document.createElement('div');
|
|
5456
5476
|
row.className = 'mini-row' + (j.enabled ? '' : ' off');
|
|
5477
|
+
/* ANLIK DURUM: sıradaki tetiklenme + son çalışma (varsa) */
|
|
5478
|
+
const live =
|
|
5479
|
+
(j.enabled && j.nextRunAt ? ` · sonraki <b>${escapeHtml(fmtWhen(j.nextRunAt))}</b>` : '') +
|
|
5480
|
+
(j.lastRunAt ? ` · son ${escapeHtml(fmtWhen(j.lastRunAt))}` : '');
|
|
5457
5481
|
row.innerHTML =
|
|
5458
5482
|
`<span class="mr-dot"></span>` +
|
|
5459
5483
|
`<div class="mr-main">` +
|
|
5460
5484
|
`<div class="mr-name">${escapeHtml(j.name || j.id)}</div>` +
|
|
5461
|
-
`<div class="mr-meta"><b>${escapeHtml(j.schedule)}</b> · ${escapeHtml(String(j.prompt || '').slice(0, 70))}</div>` +
|
|
5485
|
+
`<div class="mr-meta"><b>${escapeHtml(j.schedule)}</b> · ${escapeHtml(String(j.prompt || '').slice(0, 70))}${live}</div>` +
|
|
5462
5486
|
`</div>`;
|
|
5463
5487
|
const btns = document.createElement('div');
|
|
5464
5488
|
btns.style.cssText = 'display:flex;gap:6px;flex:none';
|
|
@@ -5488,14 +5512,41 @@ function toggleMini(overlay, hide) {
|
|
|
5488
5512
|
overlay.hidden = !!hide;
|
|
5489
5513
|
}
|
|
5490
5514
|
|
|
5515
|
+
/* CANLI YENİLEME: mini modal açık kaldığı sürece periyodik taze çekim yapar;
|
|
5516
|
+
render fonksiyonları içerik değişmemişse DOM'u yeniden kurmaz. Kapalıysa
|
|
5517
|
+
zamanlayıcı kendini durdurur. */
|
|
5518
|
+
let miniRefreshTimer = null;
|
|
5519
|
+
function startMiniRefresh() {
|
|
5520
|
+
stopMiniRefresh();
|
|
5521
|
+
miniRefreshTimer = setInterval(async () => {
|
|
5522
|
+
if (els.watchOverlay && !els.watchOverlay.hidden) await renderWatchersModal();
|
|
5523
|
+
else if (els.cronOverlay && !els.cronOverlay.hidden) await renderCronModal();
|
|
5524
|
+
else stopMiniRefresh();
|
|
5525
|
+
}, 4000);
|
|
5526
|
+
}
|
|
5527
|
+
function stopMiniRefresh() {
|
|
5528
|
+
if (miniRefreshTimer) { clearInterval(miniRefreshTimer); miniRefreshTimer = null; }
|
|
5529
|
+
}
|
|
5530
|
+
|
|
5531
|
+
function closeWatchModal() {
|
|
5532
|
+
toggleMini(els.watchOverlay, true);
|
|
5533
|
+
stopMiniRefresh();
|
|
5534
|
+
}
|
|
5535
|
+
function closeCronModal() {
|
|
5536
|
+
toggleMini(els.cronOverlay, true);
|
|
5537
|
+
stopMiniRefresh();
|
|
5538
|
+
}
|
|
5539
|
+
|
|
5491
5540
|
async function openWatchModal() {
|
|
5492
5541
|
toggleMini(els.watchOverlay, false);
|
|
5493
|
-
await renderWatchersModal();
|
|
5542
|
+
await renderWatchersModal(true); /* her basışta TAZE çekim */
|
|
5543
|
+
startMiniRefresh();
|
|
5494
5544
|
}
|
|
5495
5545
|
|
|
5496
5546
|
async function openCronModal() {
|
|
5497
5547
|
toggleMini(els.cronOverlay, false);
|
|
5498
|
-
await renderCronModal();
|
|
5548
|
+
await renderCronModal(true); /* her basışta TAZE çekim */
|
|
5549
|
+
startMiniRefresh();
|
|
5499
5550
|
}
|
|
5500
5551
|
|
|
5501
5552
|
/* ---------------- state / controls ---------------- */
|
|
@@ -5671,8 +5722,9 @@ async function sendCurrent() {
|
|
|
5671
5722
|
if (!busy) {
|
|
5672
5723
|
setBusy(true);
|
|
5673
5724
|
} else {
|
|
5674
|
-
/* agent çalışıyor — mesaj
|
|
5675
|
-
|
|
5725
|
+
/* agent çalışıyor — mesaj ANINDA konuşmaya eklenir (opencode steer):
|
|
5726
|
+
koşan tur sonraki istekte görür, eski cevap akışı bozulmaz */
|
|
5727
|
+
setStatus('mesaj ajanın akışına eklendi — sıradaki adımda değerlendirir');
|
|
5676
5728
|
}
|
|
5677
5729
|
beast.send(activeId, { text, attachments: atts });
|
|
5678
5730
|
}
|
|
@@ -6203,13 +6255,13 @@ async function init() {
|
|
|
6203
6255
|
/* cron modülü — olay bağlama */
|
|
6204
6256
|
if (els.watchBtn) els.watchBtn.addEventListener('click', openWatchModal);
|
|
6205
6257
|
if (els.cronBtn) els.cronBtn.addEventListener('click', openCronModal);
|
|
6206
|
-
if (els.watchClose) els.watchClose.addEventListener('click',
|
|
6207
|
-
if (els.cronClose) els.cronClose.addEventListener('click',
|
|
6258
|
+
if (els.watchClose) els.watchClose.addEventListener('click', closeWatchModal);
|
|
6259
|
+
if (els.cronClose) els.cronClose.addEventListener('click', closeCronModal);
|
|
6208
6260
|
if (els.watchOverlay) els.watchOverlay.addEventListener('click', (e) => {
|
|
6209
|
-
if (e.target === els.watchOverlay)
|
|
6261
|
+
if (e.target === els.watchOverlay) closeWatchModal();
|
|
6210
6262
|
});
|
|
6211
6263
|
if (els.cronOverlay) els.cronOverlay.addEventListener('click', (e) => {
|
|
6212
|
-
if (e.target === els.cronOverlay)
|
|
6264
|
+
if (e.target === els.cronOverlay) closeCronModal();
|
|
6213
6265
|
});
|
|
6214
6266
|
els.cronPreset.addEventListener('change', () => {
|
|
6215
6267
|
if (els.cronPreset.value !== '__custom') els.cronSchedule.value = els.cronPreset.value;
|
|
@@ -6294,8 +6346,8 @@ async function init() {
|
|
|
6294
6346
|
document.addEventListener('keydown', (e) => {
|
|
6295
6347
|
if (e.key === 'Escape') {
|
|
6296
6348
|
if ($('#botOverlay') && !$('#botOverlay').hidden) { botOverlaySetOpen(false); return; }
|
|
6297
|
-
if (els.watchOverlay && !els.watchOverlay.hidden) {
|
|
6298
|
-
if (els.cronOverlay && !els.cronOverlay.hidden) {
|
|
6349
|
+
if (els.watchOverlay && !els.watchOverlay.hidden) { closeWatchModal(); return; }
|
|
6350
|
+
if (els.cronOverlay && !els.cronOverlay.hidden) { closeCronModal(); return; }
|
|
6299
6351
|
if (!els.settingsOverlay.hidden) closeSettings();
|
|
6300
6352
|
}
|
|
6301
6353
|
// Ctrl+Shift+S: ekran görüntüsü al → vision'a gönderilmek üzere ekle
|