beast-agent 0.28.0 → 0.29.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 +5 -2
- package/src/agent/llm.js +153 -71
- package/src/agent/obscura.js +49 -18
- package/src/main.js +84 -22
- package/src/preload.js +1 -0
- package/src/renderer/i18n.js +26 -2
- package/src/renderer/index.html +1 -0
- package/src/renderer/renderer.js +81 -13
- package/src/renderer/style.css +27 -1
- 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
|
@@ -630,10 +630,13 @@ class Engine {
|
|
|
630
630
|
{
|
|
631
631
|
signal,
|
|
632
632
|
onDelta,
|
|
633
|
-
onRetry: (attempt) =>
|
|
633
|
+
onRetry: (attempt, st) =>
|
|
634
634
|
emitSafe(this, session.id, {
|
|
635
635
|
type: 'status',
|
|
636
|
-
status:
|
|
636
|
+
status:
|
|
637
|
+
st === undefined || st === null
|
|
638
|
+
? `bağlantı sorunu — internet dönünce ya da yeniden denemeyle sürer (${attempt}/6)`
|
|
639
|
+
: `sağlayıcı yanıtsız — tekrar deniyor (${attempt}/3)`,
|
|
637
640
|
}),
|
|
638
641
|
}
|
|
639
642
|
);
|
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) */
|
package/src/main.js
CHANGED
|
@@ -224,13 +224,10 @@ startHealthServer(); /* splash/boot aşamasından itibaren /health ayakta */
|
|
|
224
224
|
try { setSearchObscuraEnabled(settings.obscuraEnabled !== false); } catch {} /* Obscura varsayılan AKTİF */
|
|
225
225
|
try { setSearchChain(settings.searchChain); } catch {}
|
|
226
226
|
try { setTinyfishKey(settings.tinyfishKey || null); } catch {}
|
|
227
|
-
/* kurulumda Obscura da kurulsun: yoksa ARKA PLANDA otomatik indir (UI kilitlenmez)
|
|
227
|
+
/* kurulumda Obscura da kurulsun: yoksa ARKA PLANDA otomatik indir (UI kilitlenmez);
|
|
228
|
+
ilerleme Ayarlar → Web Arama'dan da izlenebilir */
|
|
228
229
|
setTimeout(() => {
|
|
229
|
-
if (!obscura.obscuraInstalled())
|
|
230
|
-
obscura.installObscura().then((r) => {
|
|
231
|
-
console.log('[obscura]', r.ok ? 'kuruldu: ' + r.dir : 'kurulamadı: ' + (r.error || '?'));
|
|
232
|
-
}).catch((e) => console.log('[obscura] kurulamadı:', String((e && e.message) || e)));
|
|
233
|
-
}
|
|
230
|
+
if (!obscura.obscuraInstalled()) startObscuraInstall();
|
|
234
231
|
}, 4000).unref?.();
|
|
235
232
|
let wa = null;
|
|
236
233
|
let waChats = new Map(); // jid -> aktif session id
|
|
@@ -634,10 +631,24 @@ function emitWaEventSafe(ev) {
|
|
|
634
631
|
|
|
635
632
|
|
|
636
633
|
|
|
634
|
+
/* sürüm: app.getVersion() + package.json yedeği (splash, /help, /version ortak) */
|
|
635
|
+
function beastVersion() {
|
|
636
|
+
try {
|
|
637
|
+
const v = app.getVersion();
|
|
638
|
+
if (v && v !== '0.0.0') return v;
|
|
639
|
+
} catch {}
|
|
640
|
+
try {
|
|
641
|
+
return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version || '?';
|
|
642
|
+
} catch {
|
|
643
|
+
return '?';
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
637
647
|
function waSlashHelp() {
|
|
638
648
|
return [
|
|
639
649
|
'*Beast komutları*',
|
|
640
650
|
'• */help* – bu liste',
|
|
651
|
+
'• */version* – Beast Agent sürümünü göster',
|
|
641
652
|
'• */new* – yeni oturum aç (kod verilir)',
|
|
642
653
|
'• */open* <kod> – o koddaki oturuma geç',
|
|
643
654
|
'• */sessions* – bu sohbetin oturumları',
|
|
@@ -1135,6 +1146,8 @@ async function tryWaSlash(jid, rawText, senderNum) {
|
|
|
1135
1146
|
out =
|
|
1136
1147
|
`*WA:* ${wst.status}${wst.user ? ' (' + wst.user + ')' : ''}\n` +
|
|
1137
1148
|
`*İzleyici:* ${watchers.list().length} adet\n*Cron:* ${jobs} aktif görev`;
|
|
1149
|
+
} else if (cmd === 'version') {
|
|
1150
|
+
out = `*Beast Agent v${beastVersion()}*\nGüncelleme için: /update (kurulum hazır olunca /update now)`;
|
|
1138
1151
|
} else {
|
|
1139
1152
|
out = `Bilinmeyen komut: /${cmd}\nListe için /help yaz.`;
|
|
1140
1153
|
}
|
|
@@ -3673,7 +3686,7 @@ function createSplash() {
|
|
|
3673
3686
|
const muted = dark ? '#9a9aa2' : '#707078';
|
|
3674
3687
|
splash = new BrowserWindow({
|
|
3675
3688
|
width: 420,
|
|
3676
|
-
height:
|
|
3689
|
+
height: 352,
|
|
3677
3690
|
frame: false,
|
|
3678
3691
|
resizable: false,
|
|
3679
3692
|
alwaysOnTop: true,
|
|
@@ -3686,6 +3699,7 @@ function createSplash() {
|
|
|
3686
3699
|
body{margin:0;height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;background:${bg};font-family:'Segoe UI',sans-serif;color:${fg}}
|
|
3687
3700
|
.logo{width:84px;height:84px;border-radius:20px;background:${fg};color:${bg};display:flex;align-items:center;justify-content:center;font-weight:900;font-size:44px}
|
|
3688
3701
|
.t{margin-top:16px;font-size:20px;color:${fg}}.t b{font-weight:900}
|
|
3702
|
+
.v{margin-top:8px;font-size:12.5px;font-weight:800;letter-spacing:.6px;color:${muted};background:${muted}22;padding:2px 12px;border-radius:9px}
|
|
3689
3703
|
.s{margin-top:6px;font-size:12px;color:${muted}}
|
|
3690
3704
|
.cmds{margin-top:14px;display:flex;flex-wrap:wrap;gap:6px;justify-content:center;max-width:360px}
|
|
3691
3705
|
.cmds b{font-size:11.5px;font-weight:800;color:${fg};background:${muted}22;padding:2px 9px;border-radius:6px;letter-spacing:.3px}
|
|
@@ -3696,9 +3710,10 @@ function createSplash() {
|
|
|
3696
3710
|
</style></head><body>
|
|
3697
3711
|
<div class="logo">B</div>
|
|
3698
3712
|
<div class="t"><b>BEAST</b> Agent</div>
|
|
3699
|
-
<div class="
|
|
3713
|
+
<div class="v">v${beastVersion()}</div>
|
|
3714
|
+
<div class="s">hızlı · hafif · becerikli</div>
|
|
3700
3715
|
<div class="cmds">
|
|
3701
|
-
<b>/help</b><b>/restart</b><b>/change</b><b>/think</b><b>/clear</b><b>/stop</b><b>/usage</b><b>/backup</b><b>/status</b><span>…</span>
|
|
3716
|
+
<b>/help</b><b>/version</b><b>/restart</b><b>/change</b><b>/think</b><b>/clear</b><b>/stop</b><b>/usage</b><b>/backup</b><b>/status</b><span>…</span>
|
|
3702
3717
|
</div>
|
|
3703
3718
|
<div class="bar"><i></i></div>
|
|
3704
3719
|
</body></html>`;
|
|
@@ -3830,6 +3845,10 @@ ipcMain.handle('agent:send', (_e, { sessionId, text }) => {
|
|
|
3830
3845
|
handleRestart(sessionId);
|
|
3831
3846
|
return true;
|
|
3832
3847
|
}
|
|
3848
|
+
if (t === '/version') {
|
|
3849
|
+
desktopEcho(sessionId, t, `**Beast Agent v${beastVersion()}**\nGüncelleme: **/update** (yeni sürüm kontrolü) · **/update now** (hemen kur)`);
|
|
3850
|
+
return true;
|
|
3851
|
+
}
|
|
3833
3852
|
if (t === '/help') {
|
|
3834
3853
|
desktopEcho(sessionId, '/help', desktopSlashHelp());
|
|
3835
3854
|
return true;
|
|
@@ -3979,6 +3998,7 @@ function desktopSlashHelp() {
|
|
|
3979
3998
|
return [
|
|
3980
3999
|
'**Beast komutları**',
|
|
3981
4000
|
'**/help** – bu liste',
|
|
4001
|
+
'**/version** – Beast Agent sürümünü göster',
|
|
3982
4002
|
'**/restart** – uygulamayı yeniden başlat',
|
|
3983
4003
|
'**/stop** – koşan işleri durdur · **/start** – devam ettir',
|
|
3984
4004
|
'**/change [n]** – modelleri listele · n. modele geç',
|
|
@@ -4081,6 +4101,13 @@ function thinkStatusText() {
|
|
|
4081
4101
|
}
|
|
4082
4102
|
|
|
4083
4103
|
/* /restart: uygulama kendini yeniden başlatır (relaunch + exit) */
|
|
4104
|
+
function scheduleAppRestart(delayMs = 800) {
|
|
4105
|
+
setTimeout(() => {
|
|
4106
|
+
try { app.relaunch(); } catch {}
|
|
4107
|
+
try { app.exit(0); } catch {}
|
|
4108
|
+
}, delayMs);
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4084
4111
|
function handleRestart(sessionId) {
|
|
4085
4112
|
const sid = String(sessionId || ''); if (win && !win.isDestroyed()) {
|
|
4086
4113
|
win.webContents.send('agent:event', { sessionId: sid, type: 'message', message: { role: 'user', content: '/restart' } });
|
|
@@ -4091,10 +4118,7 @@ function handleRestart(sessionId) {
|
|
|
4091
4118
|
});
|
|
4092
4119
|
win.webContents.send('agent:event', { sessionId: sid, type: 'done', usage: null });
|
|
4093
4120
|
}
|
|
4094
|
-
|
|
4095
|
-
try { app.relaunch(); } catch {}
|
|
4096
|
-
try { app.exit(0); } catch {}
|
|
4097
|
-
}, 800);
|
|
4121
|
+
scheduleAppRestart(800);
|
|
4098
4122
|
}
|
|
4099
4123
|
|
|
4100
4124
|
/* Masaüstünde /stop ve /start: engine'e gitmez; tüm sistemde etki eder ve
|
|
@@ -4241,6 +4265,10 @@ let netFailStreak = 0;
|
|
|
4241
4265
|
let chatQueueFlushing = false;
|
|
4242
4266
|
const chatOfflineQueue = []; // { key, sessionId, text, attachments, at }
|
|
4243
4267
|
|
|
4268
|
+
/* LLM retry'ı net izleyicisinden besler: internet kopmuşsa istek,
|
|
4269
|
+
bağlantı dönene kadar bekler — "fetch failed" ile görev ölmez */
|
|
4270
|
+
try { require('./agent/llm').setNetProbe(() => netOnline); } catch {}
|
|
4271
|
+
|
|
4244
4272
|
/* diskten yükle (app restart sonrası kuyruk korunur) */
|
|
4245
4273
|
(function chatQueueLoad() {
|
|
4246
4274
|
try {
|
|
@@ -5225,19 +5253,50 @@ ipcMain.handle('think:set', (_e, v) => {
|
|
|
5225
5253
|
});
|
|
5226
5254
|
|
|
5227
5255
|
/* Obscura stealth headless tarayıcı (Ayarlar → Web Arama) */
|
|
5256
|
+
|
|
5257
|
+
/* kurulum durumu: ayarlardan çıkılıp dönülsa da main process'te sürer;
|
|
5258
|
+
ilerleme agent:event ile panele, obscura:installState ile sekme açılışına taşınır */
|
|
5259
|
+
let obscuraInstallState = { running: false, pct: 0, phase: '', error: null };
|
|
5260
|
+
|
|
5261
|
+
function pushObscuraProgress() {
|
|
5262
|
+
try {
|
|
5263
|
+
if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'obscura-progress', ...obscuraInstallState });
|
|
5264
|
+
} catch {}
|
|
5265
|
+
}
|
|
5266
|
+
|
|
5267
|
+
function startObscuraInstall() {
|
|
5268
|
+
if (obscuraInstallState.running) return { ok: false, busy: true, ...obscuraInstallState };
|
|
5269
|
+
obscuraInstallState = { running: true, pct: 0, phase: 'hazırlanıyor', error: null };
|
|
5270
|
+
pushObscuraProgress();
|
|
5271
|
+
obscura
|
|
5272
|
+
.installObscura(null, (p) => {
|
|
5273
|
+
obscuraInstallState.pct = Math.max(0, Math.min(100, Math.round(Number(p && p.pct) || 0)));
|
|
5274
|
+
obscuraInstallState.phase = String((p && p.phase) || obscuraInstallState.phase || '');
|
|
5275
|
+
pushObscuraProgress();
|
|
5276
|
+
})
|
|
5277
|
+
.then((r) => {
|
|
5278
|
+
obscuraInstallState = r && r.ok
|
|
5279
|
+
? { running: false, pct: 100, phase: 'tamamlandı', error: null }
|
|
5280
|
+
: { running: false, pct: 0, phase: 'hata', error: String((r && r.error) || 'bilinmeyen hata') };
|
|
5281
|
+
pushObscuraProgress();
|
|
5282
|
+
console.log('[obscura]', r && r.ok ? 'kuruldu: ' + r.dir : 'kurulamadı: ' + ((r && r.error) || '?'));
|
|
5283
|
+
})
|
|
5284
|
+
.catch((e) => {
|
|
5285
|
+
obscuraInstallState = { running: false, pct: 0, phase: 'hata', error: String((e && e.message) || e) };
|
|
5286
|
+
pushObscuraProgress();
|
|
5287
|
+
console.log('[obscura] kurulamadı:', String((e && e.message) || e));
|
|
5288
|
+
});
|
|
5289
|
+
return { ok: true, started: true, ...obscuraInstallState };
|
|
5290
|
+
}
|
|
5291
|
+
|
|
5228
5292
|
ipcMain.handle('obscura:get', () => ({
|
|
5229
5293
|
installed: obscura.obscuraInstalled(),
|
|
5230
5294
|
dir: obscura.obscuraDir(),
|
|
5231
5295
|
enabled: settings.obscuraEnabled !== false,
|
|
5296
|
+
install: { ...obscuraInstallState },
|
|
5232
5297
|
}));
|
|
5233
|
-
ipcMain.handle('obscura:install',
|
|
5234
|
-
|
|
5235
|
-
const r = await obscura.installObscura();
|
|
5236
|
-
return { ok: !!r.ok, dir: r.dir || obscura.obscuraDir(), error: r.error || null };
|
|
5237
|
-
} catch (e) {
|
|
5238
|
-
return { ok: false, error: String((e && e.message) || e) };
|
|
5239
|
-
}
|
|
5240
|
-
});
|
|
5298
|
+
ipcMain.handle('obscura:install', () => startObscuraInstall());
|
|
5299
|
+
ipcMain.handle('obscura:installState', () => ({ ...obscuraInstallState }));
|
|
5241
5300
|
ipcMain.handle('obscura:setEnabled', (_e, v) => {
|
|
5242
5301
|
settings.obscuraEnabled = v !== false;
|
|
5243
5302
|
saveSettings();
|
|
@@ -6236,8 +6295,11 @@ ipcMain.handle('bots:remove', (_e, id) => {
|
|
|
6236
6295
|
saveSettings();
|
|
6237
6296
|
syncWhitelist();
|
|
6238
6297
|
log.info('main', `bot silindi: ${id} — bağlı numaralar botsuz (beast'e düşer)`);
|
|
6298
|
+
/* bot silme sonrası sistem KİTLENİYOR (oturum/DM/servis referansları) →
|
|
6299
|
+
güvenli yol: bot kaydı silindikten sonra uygulamayı temiz yeniden başlat */
|
|
6300
|
+
if (String(id || '') !== 'beast') scheduleAppRestart(1200);
|
|
6239
6301
|
}
|
|
6240
|
-
return { ...r, list: r.ok ? botListWithNumbers() : null };
|
|
6302
|
+
return { ...r, list: r.ok ? botListWithNumbers() : null, restarting: r.ok && String(id || '') !== 'beast' };
|
|
6241
6303
|
});
|
|
6242
6304
|
|
|
6243
6305
|
ipcMain.handle('bots:stats', () => botStats());
|
package/src/preload.js
CHANGED
|
@@ -72,6 +72,7 @@ contextBridge.exposeInMainWorld('beast', {
|
|
|
72
72
|
thinkSet: (v) => ipcRenderer.invoke('think:set', v),
|
|
73
73
|
obscuraGet: () => ipcRenderer.invoke('obscura:get'),
|
|
74
74
|
obscuraInstall: () => ipcRenderer.invoke('obscura:install'),
|
|
75
|
+
obscuraInstallState: () => ipcRenderer.invoke('obscura:installState'),
|
|
75
76
|
obscuraSetEnabled: (v) => ipcRenderer.invoke('obscura:setEnabled', v),
|
|
76
77
|
searchOrderGet: () => ipcRenderer.invoke('searchorder:get'),
|
|
77
78
|
searchOrderSet: (chain) => ipcRenderer.invoke('searchorder:set', chain),
|
package/src/renderer/i18n.js
CHANGED
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
tipThink: 'Düşünme (reasoning) seviyesi',
|
|
19
19
|
tipPickCfg: 'Picker\u2019da görünecek modeller',
|
|
20
20
|
tipRail: 'Paralel Ajan Konsolu',
|
|
21
|
+
tipNet: 'İnternet durumu',
|
|
22
|
+
tip_net_online: 'İnternet: bağlı',
|
|
23
|
+
tip_net_offline: 'İnternet: yok — bağlantı dönünce işler otomatik sürer',
|
|
21
24
|
tipWatch: 'İzleyiciler (Watchers)',
|
|
22
25
|
tipCron: 'Cron Görevler',
|
|
23
26
|
tip_light: 'Açık tema',
|
|
@@ -266,7 +269,14 @@
|
|
|
266
269
|
oc_status_ok: 'Obscura kurulu ve hazır.',
|
|
267
270
|
oc_status_missing: 'Obscura kurulu değil — ilk açılışta otomatik indirilir (≈74 MB).',
|
|
268
271
|
oc_install: 'Kur / Güncelle',
|
|
272
|
+
oc_install_running: 'Kuruluyor…',
|
|
269
273
|
oc_installing: 'İndiriliyor… (≈74 MB, bağlantıya göre birkaç dakika)',
|
|
274
|
+
oc_phase_prep: 'Hazırlanıyor',
|
|
275
|
+
oc_phase_download: 'İndiriliyor',
|
|
276
|
+
oc_phase_extract: 'Açılıyor',
|
|
277
|
+
oc_phase_verify: 'Doğrulanıyor',
|
|
278
|
+
oc_phase_done: 'Tamamlandı',
|
|
279
|
+
oc_bg_note: 'Kurulum arka planda sürüyor — ayarlardan çıkıp dönsen bile ilerleme burada devam eder.',
|
|
270
280
|
oc_installed_toast: 'Obscura kuruldu',
|
|
271
281
|
oc_install_fail: 'Kurulamadı: ',
|
|
272
282
|
oc_enabled: 'Obscura arama zincirinde aktif',
|
|
@@ -445,8 +455,9 @@
|
|
|
445
455
|
bot_delete: 'Botu Sil',
|
|
446
456
|
bot_confirm_del: '"${n}" silinsin mi? Bağlı numaralar botsuz duruma düşer (Beast\u2019e yönlendirilir).',
|
|
447
457
|
bot_deleted: 'Bot silindi',
|
|
458
|
+
bot_deleted_restart: 'Bot silindi — sistem temiz yeniden başlatılıyor…',
|
|
448
459
|
bot_max_toast: 'En fazla ${n} bot olabilir (1 admin + 4 müşteri)',
|
|
449
|
-
bot_name_ph: 'Bot adı (örn: Muhasebe Botu)',
|
|
460
|
+
bot_name_ph: 'Bot adı (harfle başlar, örn: Muhasebe Botu)',
|
|
450
461
|
bot_icon_ph: 'İkon seç (emoji kopyala-yapıştır)',
|
|
451
462
|
bot_created: 'Bot oluşturuldu:',
|
|
452
463
|
bot_mem_sub: 'Bu botun KENDİ hafızası — SOUL (kişilik) · USER (kullanıcı bilgisi) · MEMORY (hafıza kayıtları). Diğer botlardan tamamen izoledir.',
|
|
@@ -476,6 +487,7 @@
|
|
|
476
487
|
bot_create: 'Botu Oluştur',
|
|
477
488
|
bot_cancel: 'Vazgeç',
|
|
478
489
|
bot_name_req: 'Bot adı zorunlu',
|
|
490
|
+
bot_name_letter: 'Bot adı HARF ile başlamalı — ilk karakter harf olmalı',
|
|
479
491
|
p_preset: 'Hazır sağlayıcı (endpoint otomatik)',
|
|
480
492
|
p_preset_custom: '— elle gir —',
|
|
481
493
|
p_preset_hint: 'Hazır seçersen adres otomatik dolar; sadece API key girip modelleri çek.',
|
|
@@ -560,6 +572,9 @@
|
|
|
560
572
|
tipThink: 'Reasoning level',
|
|
561
573
|
tipPickCfg: 'Models shown in picker',
|
|
562
574
|
tipRail: 'Parallel Agents Console',
|
|
575
|
+
tipNet: 'Internet status',
|
|
576
|
+
tip_net_online: 'Internet: connected',
|
|
577
|
+
tip_net_offline: 'Internet: offline — work resumes automatically once the connection returns',
|
|
563
578
|
tipWatch: 'Watchers',
|
|
564
579
|
tipCron: 'Cron Jobs',
|
|
565
580
|
tip_light: 'Light theme',
|
|
@@ -807,7 +822,14 @@
|
|
|
807
822
|
oc_status_ok: 'Obscura is installed and ready.',
|
|
808
823
|
oc_status_missing: 'Obscura is not installed — it is auto-downloaded at first launch (≈74 MB).',
|
|
809
824
|
oc_install: 'Install / Update',
|
|
825
|
+
oc_install_running: 'Installing…',
|
|
810
826
|
oc_installing: 'Downloading… (≈74 MB, may take a few minutes)',
|
|
827
|
+
oc_phase_prep: 'Preparing',
|
|
828
|
+
oc_phase_download: 'Downloading',
|
|
829
|
+
oc_phase_extract: 'Extracting',
|
|
830
|
+
oc_phase_verify: 'Verifying',
|
|
831
|
+
oc_phase_done: 'Done',
|
|
832
|
+
oc_bg_note: 'Installation continues in the background — progress stays here even if you leave the settings.',
|
|
811
833
|
oc_installed_toast: 'Obscura installed',
|
|
812
834
|
oc_install_fail: 'Install failed: ',
|
|
813
835
|
oc_enabled: 'Obscura active in the search chain',
|
|
@@ -1044,8 +1066,9 @@
|
|
|
1044
1066
|
bot_delete: 'Delete Bot',
|
|
1045
1067
|
bot_confirm_del: 'Delete "${n}"? Linked numbers become botless (routed to Beast).',
|
|
1046
1068
|
bot_deleted: 'Bot deleted',
|
|
1069
|
+
bot_deleted_restart: 'Bot deleted — system is restarting cleanly…',
|
|
1047
1070
|
bot_max_toast: 'Maximum ${n} bots (1 admin + 4 customer)',
|
|
1048
|
-
bot_name_ph: 'Bot name (e.g. Accounting Bot)',
|
|
1071
|
+
bot_name_ph: 'Bot name (starts with a letter, e.g. Accounting Bot)',
|
|
1049
1072
|
bot_icon_ph: 'Pick an icon (emoji)',
|
|
1050
1073
|
bot_created: 'Bot created:',
|
|
1051
1074
|
bot_mem_sub: 'This bot\u2019s OWN memory — SOUL (personality) · USER (user info) · MEMORY (memory entries). Fully isolated from other bots.',
|
|
@@ -1075,6 +1098,7 @@
|
|
|
1075
1098
|
bot_create: 'Create Bot',
|
|
1076
1099
|
bot_cancel: 'Cancel',
|
|
1077
1100
|
bot_name_req: 'Bot name is required',
|
|
1101
|
+
bot_name_letter: 'Bot name must START with a letter — first character must be a letter',
|
|
1078
1102
|
p_preset: 'Built-in provider (endpoint auto-filled)',
|
|
1079
1103
|
p_preset_custom: '— manual —',
|
|
1080
1104
|
p_preset_hint: 'Pick a built-in provider to auto-fill the URL; just enter your API key and fetch models.',
|
package/src/renderer/index.html
CHANGED
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
<button id="termCBtn" class="dd-btn dd-icon-btn" title="CMD terminali" data-i18n-title="tipTermC">❯︎</button>
|
|
78
78
|
<button id="browserBtn" class="dd-btn dd-icon-btn" title="Dahili tarayıcı" data-i18n-title="tipBrowser">⧉</button>
|
|
79
79
|
<button id="eyeBtn" class="dd-btn dd-icon-btn" title="Ajan tarayıcısı görünür/gizli — göz açıkken aramalar panelde izlenir, kapalıyken gizli çalışır" data-i18n-title="tipEye"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/><line class="eye-slash" x1="4" y1="4" x2="20" y2="20"/></svg></button>
|
|
80
|
+
<span id="netDot" class="dd-btn dd-icon-btn net-on" title="İnternet: bağlı" data-i18n-title="tipNet"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/><line class="net-slash" x1="4" y1="4" x2="20" y2="20"/></svg></span>
|
|
80
81
|
<div class="drag-spacer"></div>
|
|
81
82
|
</header>
|
|
82
83
|
|
package/src/renderer/renderer.js
CHANGED
|
@@ -40,6 +40,7 @@ const els = {
|
|
|
40
40
|
todoPanel: $('#todoPanel'),
|
|
41
41
|
browserBtn: $('#browserBtn'),
|
|
42
42
|
eyeBtn: $('#eyeBtn'),
|
|
43
|
+
netDot: $('#netDot'),
|
|
43
44
|
railBtn: $('#railBtn'),
|
|
44
45
|
watchBtn: $('#watchBtn'),
|
|
45
46
|
cronBtn: $('#cronBtn'),
|
|
@@ -199,6 +200,14 @@ let netOnline = true;
|
|
|
199
200
|
let netQueueCount = 0;
|
|
200
201
|
const netPending = []; // { key, el } — bekleyen mesaj balonları
|
|
201
202
|
|
|
203
|
+
/* wifi göstergesi: bağlı = yeşil, kopuk = kırmızı */
|
|
204
|
+
function paintNetDot() {
|
|
205
|
+
if (!els.netDot) return;
|
|
206
|
+
els.netDot.classList.toggle('net-on', netOnline);
|
|
207
|
+
els.netDot.classList.toggle('net-off', !netOnline);
|
|
208
|
+
els.netDot.title = _t(netOnline ? 'tip_net_online' : 'tip_net_offline');
|
|
209
|
+
}
|
|
210
|
+
|
|
202
211
|
function setNetBadge(el, text) {
|
|
203
212
|
if (!el || !el.isConnected) return;
|
|
204
213
|
let badge = el.querySelector('.q-badge');
|
|
@@ -223,6 +232,7 @@ function onNetEvent(ev) {
|
|
|
223
232
|
if (ev.type === 'net') {
|
|
224
233
|
const was = netOnline;
|
|
225
234
|
netOnline = ev.online !== false;
|
|
235
|
+
paintNetDot();
|
|
226
236
|
if (was !== netOnline) {
|
|
227
237
|
if (netOnline) {
|
|
228
238
|
toast(_t('net_online'));
|
|
@@ -749,6 +759,50 @@ async function refreshFalloutPane() {
|
|
|
749
759
|
renderFalloutPane();
|
|
750
760
|
}
|
|
751
761
|
|
|
762
|
+
/* Obscura kurulum ilerlemesi: main process'te sürer — ayarlardan çıkılsa da
|
|
763
|
+
kesilmez. Panel açıksa çubuk canlı güncellenir; bitişte tek kez toast atılır. */
|
|
764
|
+
let _obscuraWasRunning = false;
|
|
765
|
+
function paintObscuraState(st) {
|
|
766
|
+
const ocSt = $('#ocStatus');
|
|
767
|
+
const ocProg = $('#ocProg');
|
|
768
|
+
const ocBtn = $('#ocInstall');
|
|
769
|
+
if (!ocSt || !ocProg || !ocBtn || !st || !st.phase) return;
|
|
770
|
+
const pct = Math.max(0, Math.min(100, Number(st.pct) || 0));
|
|
771
|
+
const fill = ocProg.querySelector('.oc-prog-fill');
|
|
772
|
+
const txt = ocProg.querySelector('.oc-prog-text');
|
|
773
|
+
if (st.running) {
|
|
774
|
+
ocBtn.disabled = true;
|
|
775
|
+
ocBtn.textContent = _t('oc_install_running');
|
|
776
|
+
ocProg.hidden = false;
|
|
777
|
+
if (fill) fill.style.width = pct + '%';
|
|
778
|
+
const phaseKey = ({ 'hazırlanıyor': 'prep', 'indiriliyor': 'download', 'kuruluyor': 'extract', 'doğrulanıyor': 'verify' })[st.phase];
|
|
779
|
+
if (txt) txt.textContent = (phaseKey ? _t('oc_phase_' + phaseKey) : st.phase) + ' — ' + pct + '%';
|
|
780
|
+
ocSt.textContent = _t('oc_bg_note');
|
|
781
|
+
} else if (st.phase === 'tamamlandı' && pct >= 100) {
|
|
782
|
+
ocBtn.disabled = false;
|
|
783
|
+
ocBtn.textContent = _t('oc_install');
|
|
784
|
+
ocProg.hidden = false;
|
|
785
|
+
if (fill) fill.style.width = '100%';
|
|
786
|
+
if (txt) txt.textContent = _t('oc_phase_done') + ' — 100%';
|
|
787
|
+
ocSt.textContent = _t('oc_status_ok');
|
|
788
|
+
} else if (st.phase === 'hata') {
|
|
789
|
+
ocBtn.disabled = false;
|
|
790
|
+
ocBtn.textContent = _t('oc_install');
|
|
791
|
+
ocProg.hidden = true;
|
|
792
|
+
ocSt.textContent = _t('oc_install_fail') + (st.error || '?');
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function onObscuraProgress(ev) {
|
|
797
|
+
if (ev && ev.running) _obscuraWasRunning = true;
|
|
798
|
+
paintObscuraState(ev);
|
|
799
|
+
if (ev && !ev.running && _obscuraWasRunning) {
|
|
800
|
+
_obscuraWasRunning = false;
|
|
801
|
+
if (ev.phase === 'tamamlandı') toast(_t('oc_installed_toast'));
|
|
802
|
+
else if (ev.phase === 'hata') toast(_t('oc_install_fail') + (ev.error || '?'));
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
752
806
|
/* Web Arama sekmesi: TinyFish anahtarı + Obscura (kurulum/aktiflik) + arama sırası */
|
|
753
807
|
async function renderWebSearchPane() {
|
|
754
808
|
const pane = $('#tab-websearch');
|
|
@@ -764,7 +818,10 @@ async function renderWebSearchPane() {
|
|
|
764
818
|
'<div id="ocStatus" class="sub" style="text-align:left;margin-top:8px"></div>' +
|
|
765
819
|
'<label class="mem-label" style="display:flex;align-items:center;gap:8px;cursor:pointer">' +
|
|
766
820
|
'<input id="ocEnabled" type="checkbox" style="width:auto" /> <span>' + _t('oc_enabled') + '</span></label>' +
|
|
767
|
-
'<div
|
|
821
|
+
'<div id="ocProg" class="oc-prog" hidden>' +
|
|
822
|
+
'<div class="oc-prog-bar"><div class="oc-prog-fill" style="width:0%"></div></div>' +
|
|
823
|
+
'<div class="oc-prog-text sub"></div></div>' +
|
|
824
|
+
'<div style="display:flex;justify-content:center;margin-top:12px">' +
|
|
768
825
|
'<button id="ocInstall" class="btn">' + _t('oc_install') + '</button></div>' +
|
|
769
826
|
/* --- Arama sırası --- */
|
|
770
827
|
'<div class="divider"></div>' +
|
|
@@ -786,22 +843,21 @@ async function renderWebSearchPane() {
|
|
|
786
843
|
const ocSt = $('#ocStatus');
|
|
787
844
|
const ocChk = $('#ocEnabled');
|
|
788
845
|
const ocBtn = $('#ocInstall');
|
|
789
|
-
const oc = await beast.obscuraGet().catch(() => ({ installed: false, enabled: true, dir: '' }));
|
|
846
|
+
const oc = await beast.obscuraGet().catch(() => ({ installed: false, enabled: true, dir: '', install: null }));
|
|
790
847
|
ocChk.checked = oc.enabled !== false;
|
|
791
|
-
|
|
848
|
+
/* kurulum arka planda sürüyorsa durum panelde GERİ YÜKLENİR —
|
|
849
|
+
ayarlardan çıkıp dönsen bile ilerleme kaybolmaz */
|
|
850
|
+
const ocState = oc.install || (await beast.obscuraInstallState().catch(() => null));
|
|
851
|
+
paintObscuraState(ocState);
|
|
792
852
|
ocChk.addEventListener('change', async () => {
|
|
793
853
|
await beast.obscuraSetEnabled(ocChk.checked).catch(() => {});
|
|
794
854
|
toast(ocChk.checked ? _t('oc_on_toast') : _t('oc_off_toast'));
|
|
795
855
|
});
|
|
796
856
|
ocBtn.addEventListener('click', async () => {
|
|
797
|
-
ocBtn.disabled
|
|
798
|
-
ocBtn.textContent = _t('oc_installing');
|
|
799
|
-
ocSt.textContent = _t('oc_installing');
|
|
857
|
+
if (ocBtn.disabled) return;
|
|
800
858
|
const r = await beast.obscuraInstall().catch(() => ({ ok: false, error: 'hata' }));
|
|
801
|
-
|
|
802
|
-
|
|
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 : '?'));
|
|
859
|
+
if (r && (r.ok || r.started || r.busy)) paintObscuraState(await beast.obscuraInstallState().catch(() => null));
|
|
860
|
+
else toast(_t('oc_install_fail') + ((r && r.error) || '?'));
|
|
805
861
|
});
|
|
806
862
|
|
|
807
863
|
/* --- Arama sırası --- */
|
|
@@ -3208,8 +3264,11 @@ function renderBotSettings(pane, b) {
|
|
|
3208
3264
|
})
|
|
3209
3265
|
);
|
|
3210
3266
|
$('#bSave').addEventListener('click', async () => {
|
|
3267
|
+
const newName = $('#bName').value.trim();
|
|
3268
|
+
/* İLK HARF ZORUNLU: ad değiştirilirken de harf karakteriyle başlamalı */
|
|
3269
|
+
if (newName && !/^\p{L}/u.test(newName)) { toast(_t('bot_name_letter')); $('#bName').focus(); return; }
|
|
3211
3270
|
const patch = {
|
|
3212
|
-
name:
|
|
3271
|
+
name: newName,
|
|
3213
3272
|
icon: (f.querySelector('#bIconPick button.on') || {}).dataset?.ic || b.icon,
|
|
3214
3273
|
prompt: $('#bPrompt').value,
|
|
3215
3274
|
seeBots: [...f.querySelectorAll('#bSee input:checked')].map((c) => c.dataset.see),
|
|
@@ -3235,8 +3294,12 @@ function renderBotSettings(pane, b) {
|
|
|
3235
3294
|
if (delBtn) delBtn.addEventListener('click', async () => {
|
|
3236
3295
|
if (!confirm(_ti('bot_confirm_del', b.name))) return;
|
|
3237
3296
|
const r = await beast.botsRemove(b.id);
|
|
3238
|
-
if (r.ok) {
|
|
3239
|
-
|
|
3297
|
+
if (r.ok) {
|
|
3298
|
+
/* bot silme sonrası main otomatik restart atar — yeniden çizmeye kalkışma */
|
|
3299
|
+
toast(r.restarting ? _t('bot_deleted_restart') : _t('bot_deleted'));
|
|
3300
|
+
botPageId = null;
|
|
3301
|
+
if (!r.restarting) { await refreshBots(); renderBotPage(); }
|
|
3302
|
+
} else toast(r.error || _t('bot_save_fail'));
|
|
3240
3303
|
});
|
|
3241
3304
|
/* (numara ekleme alanı kaldırıldı — Entegrasyonlar üzerinden yapılır) */
|
|
3242
3305
|
}
|
|
@@ -3358,6 +3421,8 @@ function renderBotAdd(pane) {
|
|
|
3358
3421
|
$('#bAddCreate').addEventListener('click', async () => {
|
|
3359
3422
|
const name = $('#bAddName').value.trim();
|
|
3360
3423
|
if (!name) { toast(_t('bot_name_req')); $('#bAddName').focus(); return; }
|
|
3424
|
+
/* İLK HARF ZORUNLU: bot adı harf karakteriyle başlamalı */
|
|
3425
|
+
if (!/^\p{L}/u.test(name)) { toast(_t('bot_name_letter')); $('#bAddName').focus(); return; }
|
|
3361
3426
|
const icon = (f.querySelector('#bAddIconPick button.on') || {}).dataset?.ic || '🤖';
|
|
3362
3427
|
const r = await beast.botsAdd({ name, icon, prompt: $('#bAddPrompt').value });
|
|
3363
3428
|
if (r.ok) {
|
|
@@ -3715,6 +3780,8 @@ function onEvent(ev) {
|
|
|
3715
3780
|
}
|
|
3716
3781
|
/* OFFLINE MESAJ KUYRUĞU: bağlantı + kuyruk olayları (sessionId filtresinden önce) */
|
|
3717
3782
|
if (ev.type === 'net' || ev.type === 'netQueue') { onNetEvent(ev); return; }
|
|
3783
|
+
/* Obscura kurulum ilerlemesi — ayarlardan çıkıp dönülsen de main'de sürer */
|
|
3784
|
+
if (ev.type === 'obscura-progress') { onObscuraProgress(ev); return; }
|
|
3718
3785
|
/* Beast Code oturumu (IDE modu ortasındaki panel): olayları panele akıt,
|
|
3719
3786
|
ana sohbeti kirletme */
|
|
3720
3787
|
if (bcSessionId && ev.sessionId === bcSessionId) {
|
|
@@ -4301,6 +4368,7 @@ function autosize() {
|
|
|
4301
4368
|
|
|
4302
4369
|
const SLASH_COMMANDS = [
|
|
4303
4370
|
{ cmd: '/help', desc: 'tüm komutları listele' },
|
|
4371
|
+
{ cmd: '/version', desc: 'Beast Agent sürümünü göster' },
|
|
4304
4372
|
{ cmd: '/new', desc: 'yeni oturum aç (kod verilir)' },
|
|
4305
4373
|
{ cmd: '/open ', desc: 'koddaki oturuma geç' },
|
|
4306
4374
|
{ cmd: '/sessions', desc: 'bu sohbetin oturumları' },
|
package/src/renderer/style.css
CHANGED
|
@@ -291,6 +291,13 @@ body.browser-open #topbar { width: auto; }
|
|
|
291
291
|
#eyeBtn:not(.on) { opacity: 0.55; }
|
|
292
292
|
#eyeBtn.on { background: var(--accent-dim); color: var(--accent); }
|
|
293
293
|
|
|
294
|
+
/* internet göstergesi (topbar sağ uç): bağlı = YEŞİL wifi, kopuk = KIRMIZI */
|
|
295
|
+
#netDot { cursor: default; }
|
|
296
|
+
#netDot.net-on { color: #22c55e; }
|
|
297
|
+
#netDot.net-off { color: #ef4444; }
|
|
298
|
+
#netDot.net-off .net-slash { display: block; }
|
|
299
|
+
#netDot.net-on .net-slash { display: none; }
|
|
300
|
+
|
|
294
301
|
/* #19 sağ panel (paralel ajan konsolu) aç/kapa */
|
|
295
302
|
#railBtn.on { background: var(--accent-dim); color: var(--accent); }
|
|
296
303
|
body.rail-hidden #rail { display: none; }
|
|
@@ -770,6 +777,23 @@ body.term-open #settingsOverlay { right: var(--tw, 520px); }
|
|
|
770
777
|
word-break: break-word;
|
|
771
778
|
}
|
|
772
779
|
|
|
780
|
+
/* Obscura kurulum ilerleme çubuğu (Ayarlar → Web Arama) */
|
|
781
|
+
.oc-prog { margin-top: 10px; }
|
|
782
|
+
.oc-prog-bar {
|
|
783
|
+
height: 9px;
|
|
784
|
+
background: var(--accent-dim);
|
|
785
|
+
border: 1px solid var(--border);
|
|
786
|
+
border-radius: 6px;
|
|
787
|
+
overflow: hidden;
|
|
788
|
+
}
|
|
789
|
+
.oc-prog-fill {
|
|
790
|
+
height: 100%;
|
|
791
|
+
width: 0%;
|
|
792
|
+
background: var(--accent);
|
|
793
|
+
transition: width 0.25s ease;
|
|
794
|
+
}
|
|
795
|
+
.oc-prog-text { margin-top: 5px; font-size: 12px; color: var(--muted); }
|
|
796
|
+
|
|
773
797
|
/* markdown */
|
|
774
798
|
.md p { margin: 0 0 8px; }
|
|
775
799
|
.md p:last-child { margin-bottom: 0; }
|
|
@@ -1784,14 +1808,16 @@ body.browser-open #settingsDialog { width: min(70vw, calc(100vw - var(--bw, 480p
|
|
|
1784
1808
|
flex-shrink: 0;
|
|
1785
1809
|
}
|
|
1786
1810
|
|
|
1787
|
-
/* tek tıkla model (OpenCode Zen) hero butonu */
|
|
1811
|
+
/* tek tıkla model (OpenCode Zen) hero butonu — buton ortada */
|
|
1788
1812
|
.zen-hero {
|
|
1789
1813
|
padding: 10px 12px;
|
|
1790
1814
|
border: 1px dashed var(--border);
|
|
1791
1815
|
border-radius: 10px;
|
|
1792
1816
|
margin-bottom: 12px;
|
|
1793
1817
|
background: var(--panel);
|
|
1818
|
+
text-align: center;
|
|
1794
1819
|
}
|
|
1820
|
+
.zen-hero .sub { text-align: left; }
|
|
1795
1821
|
.zen-hero .btn { min-width: 220px; }
|
|
1796
1822
|
.zen-hero .btn .zen-load {
|
|
1797
1823
|
display: inline-block;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* Bot adı disiplini: İLK KARAKTER HARF ZORUNLU (ekleme + güncelleme) */
|
|
4
|
+
|
|
5
|
+
const test = require('node:test');
|
|
6
|
+
const assert = require('node:assert');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
/* BEAST_DATA modül yüklenmeden önce ayarlanmalı (REG cache'i tek dosyada) */
|
|
12
|
+
process.env.BEAST_DATA = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-bots-name-'));
|
|
13
|
+
const bots = require('../src/agent/bots');
|
|
14
|
+
|
|
15
|
+
test('bot ekleme: ad harf ile başlamalı', () => {
|
|
16
|
+
for (const bad of ['1Muhasebe', '9bot', '-abc', '_x', ' 3BoT']) {
|
|
17
|
+
const r = bots.add({ name: bad, prompt: '' });
|
|
18
|
+
assert.strictEqual(r.ok, false, bad + ' reddedilmeli');
|
|
19
|
+
assert.ok(/harf ile başlamalı/.test(r.error), r.error);
|
|
20
|
+
}
|
|
21
|
+
const okNum = bots.add({ name: 'Muhasebe', prompt: '' });
|
|
22
|
+
assert.ok(okNum.ok, 'harf ile başlayan ad kabul: ' + (okNum.error || ''));
|
|
23
|
+
const okTr = bots.add({ name: 'Çağrı', prompt: '' });
|
|
24
|
+
assert.ok(okTr.ok, 'Türkçe harf (Ç) kabul: ' + (okTr.error || ''));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('bot güncelleme: geçersiz ad diğer alanlara dokunmadan reddedilir', () => {
|
|
28
|
+
const created = bots.add({ name: 'Depo', prompt: 'eski' });
|
|
29
|
+
assert.ok(created.ok);
|
|
30
|
+
const id = created.bot.id;
|
|
31
|
+
|
|
32
|
+
const bad = bots.update(id, { name: '3Depo', prompt: 'yeni' });
|
|
33
|
+
assert.strictEqual(bad.ok, false, 'geçersiz ad reddedildi');
|
|
34
|
+
const after = bots.get(id);
|
|
35
|
+
assert.strictEqual(after.name, 'Depo', 'eski ad korundu');
|
|
36
|
+
assert.strictEqual(after.prompt, 'eski', 'prompt değişmedi');
|
|
37
|
+
|
|
38
|
+
const good = bots.update(id, { name: 'Anbar2', prompt: 'yeni' });
|
|
39
|
+
assert.ok(good.ok, 'harf ile başlayan güncelleme kabul: ' + (good.error || ''));
|
|
40
|
+
assert.strictEqual(bots.get(id).prompt, 'yeni');
|
|
41
|
+
assert.strictEqual(bots.get(id).name, 'Anbar2');
|
|
42
|
+
});
|
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, sumUsage, chatStreamAuto } = require('../src/agent/llm');
|
|
6
|
+
const { withRetries, friendlyError, sumUsage, chatStreamAuto, setNetProbe, isNetworkError } = require('../src/agent/llm');
|
|
7
7
|
|
|
8
8
|
test('withRetries: 503 sonrası başarılı isteği tekrarlar', async () => {
|
|
9
9
|
let calls = 0;
|
|
@@ -134,3 +134,72 @@ test('chatStreamAuto: normal (stop) yanıtta hiç devam turu açılmaz', async (
|
|
|
134
134
|
srv.close();
|
|
135
135
|
}
|
|
136
136
|
});
|
|
137
|
+
|
|
138
|
+
/* ---------- ağ kopması dayanıklılığı (#retry) ---------- */
|
|
139
|
+
|
|
140
|
+
test('isNetworkError sınıflandırması', () => {
|
|
141
|
+
const net = new Error('fetch failed');
|
|
142
|
+
assert.equal(isNetworkError(net), true, 'status yok → ağ hatası');
|
|
143
|
+
const http = new Error('HTTP 503');
|
|
144
|
+
http.status = 503;
|
|
145
|
+
assert.equal(isNetworkError(http), false, 'status var → HTTP hatası');
|
|
146
|
+
const abort = new Error('iptal');
|
|
147
|
+
abort.name = 'AbortError';
|
|
148
|
+
assert.equal(isNetworkError(abort), false, 'abort ağ hatası sayılmaz');
|
|
149
|
+
assert.equal(isNetworkError(null), false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('withRetries: internet kapalıyken probe bekler, dönünce başarır', async () => {
|
|
153
|
+
let online = false;
|
|
154
|
+
setNetProbe(() => online);
|
|
155
|
+
setTimeout(() => { online = true; }, 30);
|
|
156
|
+
let calls = 0;
|
|
157
|
+
const t0 = Date.now();
|
|
158
|
+
const r = await withRetries(async () => {
|
|
159
|
+
calls++;
|
|
160
|
+
if (calls === 1) throw new Error('fetch failed');
|
|
161
|
+
return 'ok';
|
|
162
|
+
});
|
|
163
|
+
assert.equal(r, 'ok');
|
|
164
|
+
assert.equal(calls, 2);
|
|
165
|
+
assert.ok(Date.now() - t0 > 2000, 'probe çevrimdışı dediği için bekledi');
|
|
166
|
+
setNetProbe(null);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('akış ortasında bağlantı koparsa: kısmi metinle devam turu açılır (hata balonu yok)', async () => {
|
|
170
|
+
const http = require('node:http');
|
|
171
|
+
const calls = [];
|
|
172
|
+
const srv = http.createServer((req, res) => {
|
|
173
|
+
let body = '';
|
|
174
|
+
req.on('data', (c) => (body += c));
|
|
175
|
+
req.on('end', () => {
|
|
176
|
+
calls.push(JSON.parse(body).messages);
|
|
177
|
+
if (calls.length === 1) {
|
|
178
|
+
/* birkaç delta yaz, sonra soketi ORTADAN kopar */
|
|
179
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream' });
|
|
180
|
+
res.write('data: ' + JSON.stringify({ choices: [{ delta: { content: 'Merhaba du' } }] }) + '\n\n');
|
|
181
|
+
setTimeout(() => res.destroy(), 30);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream' });
|
|
185
|
+
res.write('data: ' + JSON.stringify({ choices: [{ delta: { content: 'nya devam' } }] }) + '\n\n');
|
|
186
|
+
res.write('data: ' + JSON.stringify({ choices: [{ delta: {}, finish_reason: 'stop' }] }) + '\n\n');
|
|
187
|
+
res.write('data: [DONE]\n\n');
|
|
188
|
+
res.end();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
await new Promise((r) => srv.listen(0, '127.0.0.1', r));
|
|
192
|
+
try {
|
|
193
|
+
const port = srv.address().port;
|
|
194
|
+
const sel = { url: `http://127.0.0.1:${port}/v1/chat/completions`, key: 'k', model: 'm' };
|
|
195
|
+
const r = await chatStreamAuto(sel, { messages: [{ role: 'user', content: 'selam' }] });
|
|
196
|
+
assert.equal(r.content, 'Merhaba dunya devam', 'kısmi + devam birleşti');
|
|
197
|
+
assert.equal(r.finishReason, 'stop');
|
|
198
|
+
assert.equal(calls.length, 2, 'devam turu açıldı');
|
|
199
|
+
const m2 = calls[1];
|
|
200
|
+
assert.equal(m2[m2.length - 2].role, 'assistant');
|
|
201
|
+
assert.match(m2[m2.length - 1].content, /DEVAM/);
|
|
202
|
+
} finally {
|
|
203
|
+
srv.close();
|
|
204
|
+
}
|
|
205
|
+
});
|