beast-agent 0.28.0 → 0.30.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/bots.js +8 -0
- package/src/agent/engine.js +99 -17
- package/src/agent/llm.js +153 -71
- package/src/agent/obscura.js +49 -18
- package/src/agent/skills.js +85 -27
- package/src/main.js +110 -22
- package/src/preload.js +3 -0
- package/src/renderer/i18n.js +30 -2
- package/src/renderer/index.html +1 -0
- package/src/renderer/renderer.js +173 -16
- package/src/renderer/style.css +42 -1
- package/tests/bg-jobs.test.js +45 -0
- package/tests/bots-name.test.js +42 -0
- package/tests/llm.test.js +70 -1
package/package.json
CHANGED
package/src/agent/bots.js
CHANGED
|
@@ -199,6 +199,8 @@ function add({ name, icon, prompt }) {
|
|
|
199
199
|
loadRegistry();
|
|
200
200
|
const n = String(name || '').trim().slice(0, 40);
|
|
201
201
|
if (!n) return { ok: false, error: 'bot adı zorunlu' };
|
|
202
|
+
/* İLK HARF ZORUNLU: bot adı harf karakteriyle başlamalı */
|
|
203
|
+
if (!/^\p{L}/u.test(n)) return { ok: false, error: 'bot adı harf ile başlamalı — ilk karakter harf olmalı' };
|
|
202
204
|
if (REG.bots.length >= MAX_BOTS) return { ok: false, error: `en fazla ${MAX_BOTS} bot olabilir (1 admin + ${MAX_BOTS - 1} müşteri)` };
|
|
203
205
|
let id = slugify(n);
|
|
204
206
|
while (get(id)) id = slugify(n) + '-' + uid().slice(-4);
|
|
@@ -261,6 +263,12 @@ function update(id, patch) {
|
|
|
261
263
|
loadRegistry();
|
|
262
264
|
const b = get(id);
|
|
263
265
|
if (!b) return { ok: false, error: 'bot yok' };
|
|
266
|
+
/* İLK HARF ZORUNLU: ad değişikliği harf karakteriyle başlamalı —
|
|
267
|
+
diğer alanlara dokunmadan reddet */
|
|
268
|
+
if (patch && typeof patch.name === 'string' && patch.name.trim() &&
|
|
269
|
+
!/^\p{L}/u.test(patch.name.trim())) {
|
|
270
|
+
return { ok: false, error: 'bot adı harf ile başlamalı — ilk karakter harf olmalı' };
|
|
271
|
+
}
|
|
264
272
|
const changes = [];
|
|
265
273
|
if (patch && typeof patch === 'object') {
|
|
266
274
|
if (typeof patch.name === 'string' && patch.name.trim() && patch.name.trim() !== b.name) {
|
package/src/agent/engine.js
CHANGED
|
@@ -13,6 +13,7 @@ const research = require('./research');
|
|
|
13
13
|
const memory = require('./memory');
|
|
14
14
|
const skills = require('./skills');
|
|
15
15
|
const { estTokens, estMsgTokens } = require('./tokens');
|
|
16
|
+
const log = require('./logger');
|
|
16
17
|
|
|
17
18
|
const MAX_TURNS = 25;
|
|
18
19
|
const SUB_MAX_TURNS = 8;
|
|
@@ -258,6 +259,8 @@ class Engine {
|
|
|
258
259
|
this._bgLimit = Math.max(1, Number(opts.bgLimit || process.env.BEAST_BG_LIMIT) || BG_MAX_CONCURRENT_DEFAULT);
|
|
259
260
|
this._bgPendingStart = new Map(); // sid -> ilk görev metni (slot açılınca gönderilir)
|
|
260
261
|
this._bgGroups = new Map(); // groupId -> {parentId,total,results,dead}
|
|
262
|
+
/* silinen oturum işaretleri: rapor/zincir buraya gönderilmez (hayalet oturum yok) */
|
|
263
|
+
this._deletedSessions = new Set();
|
|
261
264
|
/* #17 kalıcı ajan geçmişi: restart sonrası işler + sohbetler kaybolmasın */
|
|
262
265
|
this._bgJobsFile = path.join(this.sessionsDir, 'bg-jobs.json');
|
|
263
266
|
this._loadBgJobsPersisted();
|
|
@@ -630,10 +633,13 @@ class Engine {
|
|
|
630
633
|
{
|
|
631
634
|
signal,
|
|
632
635
|
onDelta,
|
|
633
|
-
onRetry: (attempt) =>
|
|
636
|
+
onRetry: (attempt, st) =>
|
|
634
637
|
emitSafe(this, session.id, {
|
|
635
638
|
type: 'status',
|
|
636
|
-
status:
|
|
639
|
+
status:
|
|
640
|
+
st === undefined || st === null
|
|
641
|
+
? `bağlantı sorunu — internet dönünce ya da yeniden denemeyle sürer (${attempt}/6)`
|
|
642
|
+
: `sağlayıcı yanıtsız — tekrar deniyor (${attempt}/3)`,
|
|
637
643
|
}),
|
|
638
644
|
}
|
|
639
645
|
);
|
|
@@ -1170,13 +1176,14 @@ class Engine {
|
|
|
1170
1176
|
}
|
|
1171
1177
|
|
|
1172
1178
|
deleteSession(id) {
|
|
1179
|
+
const sid = String(id || '');
|
|
1173
1180
|
try {
|
|
1174
|
-
const c = this.ctrls.get(
|
|
1181
|
+
const c = this.ctrls.get(sid);
|
|
1175
1182
|
if (c) {
|
|
1176
1183
|
this._abortReasons = this._abortReasons || new Map();
|
|
1177
|
-
this._abortReasons.set(
|
|
1184
|
+
this._abortReasons.set(sid, 'oturum silindiği için tur iptal edildi');
|
|
1178
1185
|
c.abort();
|
|
1179
|
-
this.ctrls.delete(
|
|
1186
|
+
this.ctrls.delete(sid);
|
|
1180
1187
|
}
|
|
1181
1188
|
/* #17 silinecek ajanın bekleyen öz-kurtarma zamanlayıcısını iptal et —
|
|
1182
1189
|
yoksa send() oturum dosyasını yeniden yaratıp işi diriltir */
|
|
@@ -1203,6 +1210,14 @@ class Engine {
|
|
|
1203
1210
|
this._saveBgJobs();
|
|
1204
1211
|
this._bgEmit();
|
|
1205
1212
|
}
|
|
1213
|
+
/* silinme işareti: bekleyen raporlar/rapor zinciri bu oturuma ASLA düşmez */
|
|
1214
|
+
if (this._deletedSessions) {
|
|
1215
|
+
this._deletedSessions.add(sid);
|
|
1216
|
+
if (this._deletedSessions.size > 300) {
|
|
1217
|
+
const first = this._deletedSessions.values().next().value;
|
|
1218
|
+
if (first !== undefined) this._deletedSessions.delete(first);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1206
1221
|
this.emit({ type: 'sessions' });
|
|
1207
1222
|
return true;
|
|
1208
1223
|
}
|
|
@@ -1210,23 +1225,37 @@ class Engine {
|
|
|
1210
1225
|
/* ---------- prompt assembly (the frugal part) ---------- */
|
|
1211
1226
|
|
|
1212
1227
|
/* Beast Code (IDE paneli): VS Code hızında çalışsın diye SECMET prompt.
|
|
1228
|
+
OpenCode kodlama disiplini gömülü: bağlam → plan → küçük diff → doğrula.
|
|
1213
1229
|
Hafıza embedding araması, skills taraması, kişilik/kural blokları YOK —
|
|
1214
1230
|
prompt kısa kalır → ilk token hızlı, tur maliyeti düşük. */
|
|
1215
1231
|
buildBcSystem(session) {
|
|
1216
1232
|
const nowD = new Date();
|
|
1217
1233
|
const localDate = nowD.toLocaleDateString('tr-TR');
|
|
1218
1234
|
const localTime = nowD.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' });
|
|
1235
|
+
const mode = String((session && session.bcMode) || 'auto').toLowerCase();
|
|
1236
|
+
const modeBlock =
|
|
1237
|
+
mode === 'plan'
|
|
1238
|
+
? 'ÇALIŞMA MODU: PLAN 🔍 — dosyaları oku/incele (read_file, list_dir), KOD YAZMA; mevcut yapıyı özetle ve adım adım UYGULAMA PLANI ver (hangi dosya, ne değişecek, nasıl doğrulanır). Kullanıcı /build deyince plan uygulanır.'
|
|
1239
|
+
: mode === 'build'
|
|
1240
|
+
? 'ÇALIŞMA MODU: BUILD 🛠 — son planı SOHBETTEKİ bağlamdan al ve UYGULA: dosyaları düzenle, komutları çalıştır, doğrula. Yeni plan açma; en fazla 1 cümlelik yön gösterimi + uygulama.'
|
|
1241
|
+
: 'ÇALIŞMA MODU: OTOMATİK ⚡ — önce 2-4 satırlık mini plan (todo_write), sonra hemen uygula + doğrula.';
|
|
1219
1242
|
return (
|
|
1220
|
-
'Sen BEAST CODE\u2019sun — IDE panelinde çalışan hızlı bir kodlama ajanı. VS Code gibi çevik ol.\n' +
|
|
1243
|
+
'Sen BEAST CODE\u2019sun — IDE panelinde çalışan, OpenCode disiplinli hızlı bir kodlama ajanı. VS Code gibi çevik ol.\n' +
|
|
1221
1244
|
`Çalışma klasörü: ${(session && session.workspace) || this.workspace}\n` +
|
|
1222
1245
|
`Yerel zaman: ${localDate} ${localTime}\n` +
|
|
1246
|
+
`${modeBlock}\n` +
|
|
1247
|
+
'WORKFLOW (her işte bu sıra):\n' +
|
|
1248
|
+
'1) BAĞLAM: değiştirmeden önce ilgili dosyaları OKU (read_file / list_dir / grep mantığıyla hedefli ara) — varsayım yapma, mevcut stili/konvansiyonu takip et.\n' +
|
|
1249
|
+
'2) PLAN: 2+ adımlı işlerde İLK EYLEM todo_write olsun (2-6 madde); her adım bitince status:"done" ile GÜNCELLE — liste bitene kadar iş bitmiş sayılmaz.\n' +
|
|
1250
|
+
'3) EDİT: küçük, HEDEFLİ diff\u2019ler yaz (write_file ile yalnız ilgili bölümü etkile); dosyayı baştan yazma; ilgisiz yeniden biçimleme/kayıt boşluk değişikliği YAPMA.\n' +
|
|
1251
|
+
'4) DOĞRULA: edit sonrası mümkünse derle/test et/lint çalıştır (run_command); hata varsa DÜZELT ve TEKRAR dene (en fazla 2 doğrulama turu) — kırmızı bırakma.\n' +
|
|
1252
|
+
'5) RAPOR: 1-3 satır — ne değişti + doğrulama sonucu (ör. "npm test ✓ 154/154"). Uzun açıklama yok.\n' +
|
|
1223
1253
|
'HIZ KURALLARI:\n' +
|
|
1224
|
-
'-
|
|
1225
|
-
'-
|
|
1226
|
-
'- Kurulum, test, build, git gibi işler için run_command kullan (Windows PowerShell ortamı).\n' +
|
|
1227
|
-
'- Bağımsız araç çağrılarını AYNI TURDA paralel ver.\n' +
|
|
1254
|
+
'- Kodu ve dosyaları DOĞRUDAN write_file ile yaz; basit dosya oluşturma/düzenleme için Python scripti yazma; python_run yalnız gerçek hesap/veri işleme gerekiyorsa.\n' +
|
|
1255
|
+
'- Bağımsız araç çağrılarını AYNI TURDA paralel ver (birden çok read_file tek turda).\n' +
|
|
1228
1256
|
'- Dosyayı kullanıcı söylememişse list_dir ile yapıyı görüp kendin karar ver.\n' +
|
|
1229
|
-
'-
|
|
1257
|
+
'- Run_command PowerShell ortamında çalışır (Windows).\n' +
|
|
1258
|
+
'- Soru sorma, sohbet etme; uygulayıp özetle. Kullanıcı /plan /build /auto ile modu değiştirir.\n' +
|
|
1230
1259
|
FORMAT_RULES
|
|
1231
1260
|
);
|
|
1232
1261
|
}
|
|
@@ -1798,6 +1827,11 @@ class Engine {
|
|
|
1798
1827
|
_bgGroupRecord(title, status, summary) {
|
|
1799
1828
|
for (const [gid, g] of this._bgGroups) {
|
|
1800
1829
|
if (g.dead) continue;
|
|
1830
|
+
/* üst oturum silinmişse grup ölü sayılır — hayalet oturuma yazma */
|
|
1831
|
+
if (this._deletedSessions && this._deletedSessions.has(g.parentId)) {
|
|
1832
|
+
g.dead = true;
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
1801
1835
|
g.results.push({ title, status, summary: String(summary || '').slice(0, 900) });
|
|
1802
1836
|
if (g.results.length >= g.total) {
|
|
1803
1837
|
this._bgGroups.delete(gid);
|
|
@@ -1995,8 +2029,18 @@ class Engine {
|
|
|
1995
2029
|
const reason = Engine.superviseReason(job, now);
|
|
1996
2030
|
if (!reason) continue;
|
|
1997
2031
|
/* #16 öz-kurtarma: takılan ajan CEO'yu meşgul etmeden kendini toparlar */
|
|
1998
|
-
if (reason === 'stuck'
|
|
1999
|
-
|
|
2032
|
+
if (reason === 'stuck') {
|
|
2033
|
+
if ((job.fixes || 0) < BG_FIX_MAX) {
|
|
2034
|
+
this._bgSelfHeal(job, now);
|
|
2035
|
+
} else {
|
|
2036
|
+
/* öz-kurtarma hakları tükendi + hâlâ takılı → ZORLA KAPAT:
|
|
2037
|
+
ajan sonsuza dek 'running' kalırsa fan-out grubu ASLA tamamlanmaz
|
|
2038
|
+
ve birleşik rapor hiç düşmez — CEO "uyuyor" kalır */
|
|
2039
|
+
this._bgForceClose(
|
|
2040
|
+
job,
|
|
2041
|
+
`superyorizon: ajan ${Math.round((now - Date.parse(job.startedAt)) / 60000)} dk'dır takılı, öz-kurtarma hakları tükendi`
|
|
2042
|
+
);
|
|
2043
|
+
}
|
|
2000
2044
|
continue;
|
|
2001
2045
|
}
|
|
2002
2046
|
job.lastNudgeAt = nowIso();
|
|
@@ -2037,6 +2081,26 @@ class Engine {
|
|
|
2037
2081
|
this._bgKick(job, text);
|
|
2038
2082
|
}
|
|
2039
2083
|
|
|
2084
|
+
/* Takılı işi ZORLA sonlandır: tur abort edilir, iş kaydı SEBEPİYLE kapanır,
|
|
2085
|
+
grup üyesiyse 'aborted' olarak gruba yazılır → birleşik rapor AKAR,
|
|
2086
|
+
CEO "uyumak" yerine hemen sonucu kullanıcıya aktarır. */
|
|
2087
|
+
_bgForceClose(job, reason) {
|
|
2088
|
+
const sid = String(job.id);
|
|
2089
|
+
job.revive = false; /* kick-in-flight guard'ı devre dışı — bu gerçek bir bitiş */
|
|
2090
|
+
this._clearKicks(sid);
|
|
2091
|
+
this._abortReasons = this._abortReasons || new Map();
|
|
2092
|
+
this._abortReasons.set(sid, this._abortReason(reason));
|
|
2093
|
+
const ctrl = this.ctrls.get(sid);
|
|
2094
|
+
if (ctrl) {
|
|
2095
|
+
try { ctrl.abort(); } catch {}
|
|
2096
|
+
/* _run catch'i sebebi okuyup _bgFinish('aborted') yapacak */
|
|
2097
|
+
} else {
|
|
2098
|
+
/* tur zaten yok — kaydı doğrudan kapat */
|
|
2099
|
+
this._bgFinish(sid, 'aborted', reason);
|
|
2100
|
+
}
|
|
2101
|
+
try { log.info('bg', `iş zorla kapatıldı: ${job.title} — ${reason}`); } catch {}
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2040
2104
|
/* Arka plan oturumuna müdahale: asılı turu kes (ctrl.abort), sonra
|
|
2041
2105
|
kurtarma mesajını bas. abort → eski _run sonlanır → send yeni tur açar.
|
|
2042
2106
|
revive bayrağı: /stop veya silme sonrası bekleyen müdahaleler patlamasın. */
|
|
@@ -2090,6 +2154,11 @@ class Engine {
|
|
|
2090
2154
|
try {
|
|
2091
2155
|
const meta = this._bgMeta && this._bgMeta.get(String(bgSessionId));
|
|
2092
2156
|
if (!meta) return false;
|
|
2157
|
+
/* üst oturum silinmişse rapor düşer — hayalet oturum yaratma */
|
|
2158
|
+
if (this._deletedSessions && this._deletedSessions.has(meta.parentId)) {
|
|
2159
|
+
this._bgMeta.delete(String(bgSessionId));
|
|
2160
|
+
return false;
|
|
2161
|
+
}
|
|
2093
2162
|
/* Öz-kurtarma/wrap-up kick'i bekliyorsa bu bir ABORT turudur — gerçek
|
|
2094
2163
|
bitiş DEĞİL; erken "ARKA PLAN BİTTİ (sonuca ulaşmadan)" ilanı basma */
|
|
2095
2164
|
const pendingKick = this._bgJobs.get(String(bgSessionId));
|
|
@@ -2131,14 +2200,19 @@ class Engine {
|
|
|
2131
2200
|
}
|
|
2132
2201
|
}
|
|
2133
2202
|
|
|
2134
|
-
/* Ana oturum işini bitirince bekleyen arka plan raporlarını boşalt
|
|
2203
|
+
/* Ana oturum işini bitirince bekleyen arka plan raporlarını boşalt.
|
|
2204
|
+
Güvenli: oturum SİLİNMİŞSE rapor düşer (hayalet oturum yaratılmaz);
|
|
2205
|
+
send başarısızsa (aniden meşgul oldu) rapor sırada kalır, sonra denenir. */
|
|
2135
2206
|
flushPendingReports(sessionId) {
|
|
2136
2207
|
if (!this._pendingReports || !this._pendingReports.length) return;
|
|
2137
2208
|
const rest = [];
|
|
2138
2209
|
for (const r of this._pendingReports) {
|
|
2139
|
-
if (r.parentId
|
|
2140
|
-
|
|
2141
|
-
|
|
2210
|
+
if (r.parentId !== sessionId) { rest.push(r); continue; }
|
|
2211
|
+
if (this._deletedSessions && this._deletedSessions.has(sessionId)) continue;
|
|
2212
|
+
if (this.isBusy(sessionId)) { rest.push(r); continue; }
|
|
2213
|
+
let sent = false;
|
|
2214
|
+
try { sent = !!this.send(sessionId, { text: r.text }); } catch {}
|
|
2215
|
+
if (!sent) rest.push(r);
|
|
2142
2216
|
}
|
|
2143
2217
|
this._pendingReports = rest;
|
|
2144
2218
|
}
|
|
@@ -2471,6 +2545,14 @@ class Engine {
|
|
|
2471
2545
|
} finally {
|
|
2472
2546
|
this.ctrls.delete(sid);
|
|
2473
2547
|
emit({ type: 'status', status: 'idle' });
|
|
2548
|
+
/* bekleyen paralel-ajan raporları HER bitiş yolunda boşaltılır —
|
|
2549
|
+
hata/durdurma/tur-limiti sonrası bile sonuç raporu kullanıcıya ulaşır.
|
|
2550
|
+
DİKKAT: buradan flush'u DOĞRUDAN çağırma! send → _run senkron-hata
|
|
2551
|
+
yolunda (model-yok gibi) await'siz zincirlenir → stack taşması.
|
|
2552
|
+
Macrotask'a ertele: her hop taze stack alır, pop-gönder sırası bozulmaz. */
|
|
2553
|
+
const fl = () => { try { this.flushPendingReports(sid); } catch {} };
|
|
2554
|
+
const t = setTimeout(fl, 0);
|
|
2555
|
+
if (t.unref) t.unref();
|
|
2474
2556
|
}
|
|
2475
2557
|
}
|
|
2476
2558
|
|
package/src/agent/llm.js
CHANGED
|
@@ -1,10 +1,51 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/* OpenAI-compatible streaming chat client with SSE parsing.
|
|
4
|
-
Geçici sağlayıcı hatalarında (5xx/429
|
|
4
|
+
Geçici sağlayıcı hatalarında (5xx/429) üstel beklemeyle retry; İNTERNET
|
|
5
|
+
KOPMASINDA (fetch failed) çok daha sabırlı: 6 deneme + internet dönene
|
|
6
|
+
dek bekleme (main'deki net izleyicisinden beslenir). Akış ortasında kopan
|
|
7
|
+
bağlantıda: hiç veri akmadıysa sessiz baştan dener, kısmi metin geldiyse
|
|
8
|
+
'length' gibi döner → chatStreamAuto kaldığı yerden DEVAM ETTİRİR. */
|
|
5
9
|
|
|
6
10
|
const RETRYABLE_STATUS = new Set([408, 409, 425, 429, 500, 502, 503, 504]);
|
|
7
|
-
const MAX_RETRIES = 3;
|
|
11
|
+
const MAX_RETRIES = 3; /* sağlayıcı geçici HTTP hataları */
|
|
12
|
+
const NET_MAX_RETRIES = 6; /* bağlantı kopması — daha sabırlı */
|
|
13
|
+
const NET_BACKOFF = [1000, 2000, 4000, 8000, 15000, 25000]; /* ≈55 sn toplam */
|
|
14
|
+
const NET_WAIT_CAP_MS = 120000; /* internet dönene dek bekleme üst sınırı */
|
|
15
|
+
|
|
16
|
+
/* main process'teki net izleyicisi besler: () => boolean | Promise<boolean> */
|
|
17
|
+
let _netProbe = null;
|
|
18
|
+
function setNetProbe(fn) {
|
|
19
|
+
_netProbe = typeof fn === 'function' ? fn : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* fetch failed / ENOTFOUND / ECONNRESET / socket hang up / terminated —
|
|
23
|
+
HTTP yanıtı ALINAMADI demektir (status yok) */
|
|
24
|
+
function isNetworkError(e) {
|
|
25
|
+
if (!e) return false;
|
|
26
|
+
if (e.name === 'AbortError') return false;
|
|
27
|
+
if (e.status !== undefined) return false;
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* internet yoksa dönene kadar bekle (en fazla NET_WAIT_CAP_MS); probe yoksa
|
|
32
|
+
ya da durum bilinmiyorsa hemen geç */
|
|
33
|
+
async function waitForNet(signal) {
|
|
34
|
+
if (!_netProbe) return true;
|
|
35
|
+
const t0 = Date.now();
|
|
36
|
+
for (;;) {
|
|
37
|
+
if (signal && signal.aborted) {
|
|
38
|
+
const e = new Error('iptal');
|
|
39
|
+
e.name = 'AbortError';
|
|
40
|
+
throw e;
|
|
41
|
+
}
|
|
42
|
+
let online;
|
|
43
|
+
try { online = await _netProbe(); } catch { online = null; }
|
|
44
|
+
if (online !== false) return true; /* false DEĞİLSE devam (bilinmiyor = iyimser) */
|
|
45
|
+
if (Date.now() - t0 > NET_WAIT_CAP_MS) return false;
|
|
46
|
+
await sleep(2000, signal);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
8
49
|
|
|
9
50
|
const HINTS = {
|
|
10
51
|
401: 'API anahtarı geçersiz',
|
|
@@ -47,8 +88,9 @@ function sleep(ms, signal) {
|
|
|
47
88
|
});
|
|
48
89
|
}
|
|
49
90
|
|
|
50
|
-
/* fn: denenecek istek.
|
|
51
|
-
|
|
91
|
+
/* fn: denenecek istek. Sağlayıcı geçici HTTP hatalarında 0.8s→1.6s→3.2s;
|
|
92
|
+
İNTERNET KOPMASINDA (status yok) 6 denemeye kadar 1s→2s→4s→8s→15s→25s ve
|
|
93
|
+
net izleyicisi "çevrimdışı" diyorsa internet dönene kadar bekler. */
|
|
52
94
|
async function withRetries(fn, { signal, onRetry } = {}) {
|
|
53
95
|
let attempt = 0;
|
|
54
96
|
for (;;) {
|
|
@@ -57,14 +99,23 @@ async function withRetries(fn, { signal, onRetry } = {}) {
|
|
|
57
99
|
} catch (e) {
|
|
58
100
|
const aborted = e && (e.name === 'AbortError' || (signal && signal.aborted));
|
|
59
101
|
const status = e && e.status;
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
if (!retriable
|
|
102
|
+
const netErr = !aborted && status === undefined;
|
|
103
|
+
const retriable = !aborted && (status === undefined || RETRYABLE_STATUS.has(status));
|
|
104
|
+
if (!retriable) throw e;
|
|
105
|
+
const cap = netErr ? NET_MAX_RETRIES : MAX_RETRIES;
|
|
106
|
+
if (attempt >= cap) throw e;
|
|
107
|
+
if (netErr && _netProbe) {
|
|
108
|
+
/* internet kopmuş: dönene kadar bekle — kısa kopmalarda görev ölmez */
|
|
109
|
+
await waitForNet(signal);
|
|
110
|
+
}
|
|
63
111
|
attempt++;
|
|
64
112
|
try {
|
|
65
113
|
onRetry && onRetry(attempt, status);
|
|
66
114
|
} catch {}
|
|
67
|
-
|
|
115
|
+
const wait = netErr
|
|
116
|
+
? NET_BACKOFF[Math.min(attempt - 1, NET_BACKOFF.length - 1)]
|
|
117
|
+
: 800 * Math.pow(2, attempt - 1);
|
|
118
|
+
await sleep(wait, signal);
|
|
68
119
|
}
|
|
69
120
|
}
|
|
70
121
|
}
|
|
@@ -113,79 +164,101 @@ async function chatStream(sel, body, { signal, onDelta, onRetry } = {}) {
|
|
|
113
164
|
}
|
|
114
165
|
|
|
115
166
|
async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReasoning = false) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
167
|
+
/* akış durumu dışarıda tutulur: gövde okunurken bağlantı koparsa
|
|
168
|
+
ne kadarı geldiğine bakılır (kısmi metin → devam; boş/araç → baştan) */
|
|
169
|
+
const state = { content: '', reasoning: '', toolCalls: [], usage: null, finishReason: null };
|
|
170
|
+
let dropAttempt = 0;
|
|
171
|
+
|
|
172
|
+
for (;;) {
|
|
173
|
+
const res = await withRetries(
|
|
174
|
+
async () => {
|
|
175
|
+
const r = await openChat(sel, body, { stream: true, signal, omitReasoning });
|
|
176
|
+
if (!r.ok) {
|
|
177
|
+
let detail = '';
|
|
178
|
+
try {
|
|
179
|
+
detail = (await r.text()).slice(0, 300);
|
|
180
|
+
} catch {}
|
|
181
|
+
const err = new Error(friendlyError(r.status, r.statusText, detail));
|
|
182
|
+
err.status = r.status;
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
return r;
|
|
186
|
+
},
|
|
187
|
+
{ signal, onRetry }
|
|
188
|
+
);
|
|
132
189
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
let finishReason = null;
|
|
190
|
+
try {
|
|
191
|
+
const reader = res.body.getReader();
|
|
192
|
+
const decoder = new TextDecoder();
|
|
193
|
+
let buf = '';
|
|
138
194
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
195
|
+
while (true) {
|
|
196
|
+
const { done, value } = await reader.read();
|
|
197
|
+
if (done) break;
|
|
198
|
+
buf += decoder.decode(value, { stream: true });
|
|
142
199
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
200
|
+
let idx;
|
|
201
|
+
while ((idx = buf.indexOf('\n')) !== -1) {
|
|
202
|
+
const line = buf.slice(0, idx).trim();
|
|
203
|
+
buf = buf.slice(idx + 1);
|
|
204
|
+
if (!line.startsWith('data:')) continue;
|
|
205
|
+
const data = line.slice(5).trim();
|
|
206
|
+
if (!data || data === '[DONE]') continue;
|
|
147
207
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (!data || data === '[DONE]') continue;
|
|
208
|
+
let json;
|
|
209
|
+
try {
|
|
210
|
+
json = JSON.parse(data);
|
|
211
|
+
} catch {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
155
214
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
215
|
+
if (json.usage) state.usage = json.usage;
|
|
216
|
+
const ch = json.choices && json.choices[0];
|
|
217
|
+
if (!ch) continue;
|
|
218
|
+
if (ch.finish_reason) state.finishReason = ch.finish_reason;
|
|
219
|
+
const d = ch.delta || {};
|
|
220
|
+
if (d.content) {
|
|
221
|
+
state.content += d.content;
|
|
222
|
+
onDelta && onDelta(d.content, state.content);
|
|
223
|
+
}
|
|
224
|
+
if (d.reasoning_content) state.reasoning += d.reasoning_content;
|
|
225
|
+
if (d.reasoning) state.reasoning += d.reasoning;
|
|
226
|
+
for (const tc of d.tool_calls || []) {
|
|
227
|
+
const i = typeof tc.index === 'number' ? tc.index : state.toolCalls.length;
|
|
228
|
+
while (state.toolCalls.length <= i) {
|
|
229
|
+
state.toolCalls.push({ id: '', type: 'function', function: { name: '', arguments: '' } });
|
|
230
|
+
}
|
|
231
|
+
if (tc.id) state.toolCalls[i].id = tc.id;
|
|
232
|
+
if (tc.function) {
|
|
233
|
+
if (tc.function.name) state.toolCalls[i].function.name += tc.function.name;
|
|
234
|
+
if (tc.function.arguments) state.toolCalls[i].function.arguments += tc.function.arguments;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
161
238
|
}
|
|
162
239
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (d.reasoning) reasoning += d.reasoning;
|
|
174
|
-
for (const tc of d.tool_calls || []) {
|
|
175
|
-
const i = typeof tc.index === 'number' ? tc.index : toolCalls.length;
|
|
176
|
-
while (toolCalls.length <= i) {
|
|
177
|
-
toolCalls.push({ id: '', type: 'function', function: { name: '', arguments: '' } });
|
|
178
|
-
}
|
|
179
|
-
if (tc.id) toolCalls[i].id = tc.id;
|
|
180
|
-
if (tc.function) {
|
|
181
|
-
if (tc.function.name) toolCalls[i].function.name += tc.function.name;
|
|
182
|
-
if (tc.function.arguments) toolCalls[i].function.arguments += tc.function.arguments;
|
|
240
|
+
return { ...state };
|
|
241
|
+
} catch (e) {
|
|
242
|
+
/* okuma sırasında bağlantı koptu */
|
|
243
|
+
const aborted = e && (e.name === 'AbortError' || (signal && signal.aborted));
|
|
244
|
+
if (!aborted && isNetworkError(e)) {
|
|
245
|
+
if (state.content && !state.toolCalls.length) {
|
|
246
|
+
/* kısmi metin geldi, araç çağrısı yok → hata balonu YOK:
|
|
247
|
+
'length' gibi dön, chatStreamAuto CONTINUE_PROMPT ile kaldığı
|
|
248
|
+
yerden sürdürür ve metni birleştirir */
|
|
249
|
+
return { ...state, finishReason: 'length' };
|
|
183
250
|
}
|
|
251
|
+
/* hiç veri akmadı YA DA araç çağrısı yarım kaldı → BAŞTAN dene */
|
|
252
|
+
if (dropAttempt >= NET_MAX_RETRIES) throw e;
|
|
253
|
+
dropAttempt++;
|
|
254
|
+
try { onRetry && onRetry(dropAttempt, undefined); } catch {}
|
|
255
|
+
if (_netProbe) await waitForNet(signal);
|
|
256
|
+
await sleep(NET_BACKOFF[Math.min(dropAttempt - 1, NET_BACKOFF.length - 1)], signal);
|
|
257
|
+
continue; /* yeni stream — UI'daki kısmi token'lar nihai mesajla ezilir */
|
|
184
258
|
}
|
|
259
|
+
throw e;
|
|
185
260
|
}
|
|
186
261
|
}
|
|
187
|
-
|
|
188
|
-
return { content, reasoning, toolCalls, usage, finishReason };
|
|
189
262
|
}
|
|
190
263
|
|
|
191
264
|
/* usage toplama: devam turları gerçek faturalamayı yansıtsın (prompt yeniden sayılır) */
|
|
@@ -261,4 +334,13 @@ async function chatOnce(sel, body, { signal, onRetry } = {}) {
|
|
|
261
334
|
};
|
|
262
335
|
}
|
|
263
336
|
|
|
264
|
-
module.exports = {
|
|
337
|
+
module.exports = {
|
|
338
|
+
chatStream,
|
|
339
|
+
chatStreamAuto,
|
|
340
|
+
chatOnce,
|
|
341
|
+
withRetries,
|
|
342
|
+
friendlyError,
|
|
343
|
+
sumUsage,
|
|
344
|
+
setNetProbe,
|
|
345
|
+
isNetworkError,
|
|
346
|
+
};
|
package/src/agent/obscura.js
CHANGED
|
@@ -20,6 +20,8 @@ const RELEASE_BASE = 'https://github.com/h4ckf0r0day/obscura/releases/latest/dow
|
|
|
20
20
|
const WINDOWS_ZIP = 'obscura-x86_64-windows-stealth.zip';
|
|
21
21
|
const MAX_ZIP_BYTES = 250 * 1024 * 1024;
|
|
22
22
|
const FETCH_TIMEOUT_MS = 25000;
|
|
23
|
+
/* content-length gelmezse yüzde tahmini için yaklaşık boyut (stealth zip ≈74 MB) */
|
|
24
|
+
const EST_ZIP_BYTES = 74 * 1024 * 1024;
|
|
23
25
|
|
|
24
26
|
function obscuraDir() {
|
|
25
27
|
const base = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
@@ -58,19 +60,29 @@ function obscuraOk() {
|
|
|
58
60
|
return _okProbe;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
function httpsDownload(url, redirectsLeft = 5, signal) {
|
|
63
|
+
function httpsDownload(url, redirectsLeft = 5, signal, onProgress) {
|
|
62
64
|
return new Promise((resolve, reject) => {
|
|
63
65
|
const req = https.get(url, { headers: { 'User-Agent': 'BeastAgent/1.0 (+obscura bootstrap)' } }, (res) => {
|
|
64
66
|
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location && redirectsLeft > 0) {
|
|
65
67
|
res.resume();
|
|
66
|
-
return resolve(httpsDownload(new URL(res.headers.location, url).toString(), redirectsLeft - 1, signal));
|
|
68
|
+
return resolve(httpsDownload(new URL(res.headers.location, url).toString(), redirectsLeft - 1, signal, onProgress));
|
|
67
69
|
}
|
|
68
70
|
if (res.statusCode !== 200) {
|
|
69
71
|
res.resume();
|
|
70
72
|
return reject(new Error(`indirme başarısız: HTTP ${res.statusCode}`));
|
|
71
73
|
}
|
|
74
|
+
const total = Number(res.headers['content-length']) || EST_ZIP_BYTES;
|
|
72
75
|
const chunks = [];
|
|
73
76
|
let size = 0;
|
|
77
|
+
let lastPct = -1;
|
|
78
|
+
const tick = () => {
|
|
79
|
+
/* yüzdeyi INDIRME fazının %1-88 aralığına map et; %1'lik adımda bildir */
|
|
80
|
+
const pct = 1 + Math.min(88, Math.floor((size / total) * 88));
|
|
81
|
+
if (typeof onProgress === 'function' && pct !== lastPct) {
|
|
82
|
+
lastPct = pct;
|
|
83
|
+
try { onProgress({ pct, phase: 'indiriliyor', loaded: size, total }); } catch {}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
74
86
|
res.on('data', (c) => {
|
|
75
87
|
size += c.length;
|
|
76
88
|
if (size > MAX_ZIP_BYTES) {
|
|
@@ -78,8 +90,12 @@ function httpsDownload(url, redirectsLeft = 5, signal) {
|
|
|
78
90
|
return;
|
|
79
91
|
}
|
|
80
92
|
chunks.push(c);
|
|
93
|
+
tick();
|
|
94
|
+
});
|
|
95
|
+
res.on('end', () => {
|
|
96
|
+
tick();
|
|
97
|
+
resolve(Buffer.concat(chunks));
|
|
81
98
|
});
|
|
82
|
-
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
83
99
|
res.on('error', reject);
|
|
84
100
|
});
|
|
85
101
|
req.on('error', reject);
|
|
@@ -114,22 +130,37 @@ function psExpand(zipPath, destDir) {
|
|
|
114
130
|
});
|
|
115
131
|
}
|
|
116
132
|
|
|
117
|
-
/* Obscura'yı kur / güncelle (yeniden indirip üzerine yazar)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
/* Obscura'yı kur / güncelle (yeniden indirip üzerine yazar).
|
|
134
|
+
onProgress({pct, phase}) — 1-88 indirme, 89-96 açma, 97-99 doğrulama. */
|
|
135
|
+
async function installObscura(signal, onProgress) {
|
|
136
|
+
const tick = (pct, phase) => {
|
|
137
|
+
if (typeof onProgress === 'function') {
|
|
138
|
+
try { onProgress({ pct, phase: phase || 'indiriliyor' }); } catch {}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
try {
|
|
142
|
+
tick(0, 'hazırlanıyor');
|
|
143
|
+
const dest = obscuraDir();
|
|
144
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
145
|
+
const zipPath = path.join(os.tmpdir(), 'beast-obscura.zip');
|
|
146
|
+
const buf = await httpsDownload(RELEASE_BASE + WINDOWS_ZIP, 5, signal, onProgress);
|
|
147
|
+
tick(89, 'kuruluyor');
|
|
148
|
+
fs.writeFileSync(zipPath, buf);
|
|
149
|
+
tick(90, 'kuruluyor');
|
|
150
|
+
const ok = await psExpand(zipPath, dest);
|
|
151
|
+
try { fs.unlinkSync(zipPath); } catch {}
|
|
152
|
+
tick(97, 'doğrulanıyor');
|
|
153
|
+
if (!ok || !obscuraInstalled()) {
|
|
154
|
+
return { ok: false, error: 'obscura kurulumu başarısız (zip açılamadı)' };
|
|
155
|
+
}
|
|
156
|
+
markVariant('stealth');
|
|
157
|
+
_okProbe = null; /* yeniden doğrula */
|
|
158
|
+
const good = await obscuraOk();
|
|
159
|
+
tick(good ? 100 : 98, good ? 'tamamlandı' : 'doğrulanıyor');
|
|
160
|
+
return { ok: good, dir: dest, error: good ? null : 'obscura.exe doğrulanamadı' };
|
|
161
|
+
} catch (e) {
|
|
162
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
128
163
|
}
|
|
129
|
-
markVariant('stealth');
|
|
130
|
-
_okProbe = null; /* yeniden doğrula */
|
|
131
|
-
const good = await obscuraOk();
|
|
132
|
-
return { ok: good, dir: dest, error: good ? null : 'obscura.exe doğrulanamadı' };
|
|
133
164
|
}
|
|
134
165
|
|
|
135
166
|
/* obscura fetch — sayfa HTML'i (abort ile süreç kesilir) */
|