beast-agent 2.34.0 → 2.35.1
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 +12 -1
- package/src/agent/llm.js +1 -16
- package/src/agent/squeeze.js +312 -0
- package/src/main.js +62 -64
- package/src/preload.js +4 -3
- package/src/renderer/i18n.js +2 -2
- package/src/renderer/index.html +2 -2
- package/src/renderer/renderer.js +63 -91
- package/src/agent/headroom.js +0 -279
package/package.json
CHANGED
package/src/agent/engine.js
CHANGED
|
@@ -25,6 +25,7 @@ const mcp = require('./mcp');
|
|
|
25
25
|
const apps = require('./apps');
|
|
26
26
|
const pdfwrite = require('./pdfwrite');
|
|
27
27
|
const { estTokens, estMsgTokens } = require('./tokens');
|
|
28
|
+
const squeeze = require('./squeeze');
|
|
28
29
|
const log = require('./logger');
|
|
29
30
|
/* opencode mantığı (birebir port): ajanlar, izinler, tool seti, promptlar —
|
|
30
31
|
Beast Code (bcCode) oturumları bu çekirdek üzerinden koşar */
|
|
@@ -767,6 +768,12 @@ class Engine {
|
|
|
767
768
|
413/TPM "Requested N" hatası → gerçek token sayısı öğrenilir, tahmin
|
|
768
769
|
kalibre edilir ve aynı model BİR KEZ daha sıkı sıkıştırmayla denenir. */
|
|
769
770
|
async _streamWithFallbacks(session, payload, activeTools, signal, onDelta, sel, wasVision) {
|
|
771
|
+
/* SQUEEZE (varsayılan kapalı): istek kopyası sıkıştırılır — oturum kaydı
|
|
772
|
+
değişmez. Provider limiti kontrolünden ÖNCE uygulanır ki limit hesabı
|
|
773
|
+
küçülmüş gövdeyi görsün. */
|
|
774
|
+
if (squeeze.isEnabled()) {
|
|
775
|
+
payload = squeeze.apply(payload, { cacheKey: String(session.id || '') });
|
|
776
|
+
}
|
|
770
777
|
const cands = this._chatCandidates(sel, wasVision);
|
|
771
778
|
let lastErr = null;
|
|
772
779
|
for (let i = 0; i < cands.length; i++) {
|
|
@@ -4773,10 +4780,14 @@ const skills = require('./skills');
|
|
|
4773
4780
|
];
|
|
4774
4781
|
try {
|
|
4775
4782
|
for (let turn = 0; turn < SUB_MAX_TURNS; turn++) {
|
|
4783
|
+
let sendMsgs = [{ role: 'system', content: system }, ...msgs.slice(1)];
|
|
4784
|
+
if (squeeze.isEnabled()) {
|
|
4785
|
+
sendMsgs = squeeze.apply(sendMsgs, { cacheKey: String(sessionId || 'subagent') });
|
|
4786
|
+
}
|
|
4776
4787
|
const res = await chatStreamAuto(
|
|
4777
4788
|
sel,
|
|
4778
4789
|
{
|
|
4779
|
-
messages:
|
|
4790
|
+
messages: sendMsgs,
|
|
4780
4791
|
tools: subTools,
|
|
4781
4792
|
reasoningEffort: this._thinkEffort(),
|
|
4782
4793
|
cacheKey: String(sessionId || 'subagent'),
|
package/src/agent/llm.js
CHANGED
|
@@ -347,18 +347,6 @@ function sessionHeaderId(body) {
|
|
|
347
347
|
return _sessionFallback;
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
-
/* Headroom proxy yönlendirme: providerId -> yerel proxy portu haritası.
|
|
351
|
-
main (agent/headroom.js) ayarlardan açıldığında kurar; null = düz bağlan. */
|
|
352
|
-
let _headroomRoutes = null;
|
|
353
|
-
function setHeadroomRoutes(map) {
|
|
354
|
-
_headroomRoutes = map && Object.keys(map).length ? map : null;
|
|
355
|
-
}
|
|
356
|
-
function headroomUrlFor(sel) {
|
|
357
|
-
if (!_headroomRoutes || !sel) return '';
|
|
358
|
-
const port = Number(_headroomRoutes[String(sel.providerId || '')] || 0);
|
|
359
|
-
return port ? 'http://127.0.0.1:' + port + '/v1/chat/completions' : '';
|
|
360
|
-
}
|
|
361
|
-
|
|
362
350
|
async function openChat(sel, body, { stream, signal, drops } = {}) {
|
|
363
351
|
const d = drops || {};
|
|
364
352
|
const payload = {
|
|
@@ -380,9 +368,7 @@ async function openChat(sel, body, { stream, signal, drops } = {}) {
|
|
|
380
368
|
if (body.cacheKey && wantsCacheKey(sel) && !d.noCacheKey) {
|
|
381
369
|
payload.prompt_cache_key = String(body.cacheKey);
|
|
382
370
|
}
|
|
383
|
-
|
|
384
|
-
yoksa sağlayıcıya düz bağlan */
|
|
385
|
-
const target = headroomUrlFor(sel) || sel.url;
|
|
371
|
+
const target = sel.url;
|
|
386
372
|
return fetch(target, {
|
|
387
373
|
method: 'POST',
|
|
388
374
|
signal,
|
|
@@ -641,7 +627,6 @@ module.exports = {
|
|
|
641
627
|
capsFor,
|
|
642
628
|
resetCaps,
|
|
643
629
|
learnCaps,
|
|
644
|
-
setHeadroomRoutes,
|
|
645
630
|
PARAM_LABELS,
|
|
646
631
|
isNetworkError,
|
|
647
632
|
wantsCacheKey,
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* Beast SQUEEZE — kendi bağlam (token) sıkıştırıcımız. VARSAYILAN KAPALI.
|
|
4
|
+
Her LLM isteğinden hemen önce mesaj dizisinin KOPYASINA uygulanır; oturum
|
|
5
|
+
kaydı/geçmiş DEĞİŞMEZ. Harici kurulum/proxy yok.
|
|
6
|
+
|
|
7
|
+
Kurallar:
|
|
8
|
+
1) TEKRAR BLOK: Aynı büyük içerik (araç çıktısı / dosya eki / skill gövdesi)
|
|
9
|
+
aynı bağlamda birden çok kez varsa YALNIZ İLK kopya tam gider; sonrakiler
|
|
10
|
+
kısa işaretçiye iner. İlk kopya bağlamdan düşerse (compaction/bütçe) sonraki
|
|
11
|
+
kopya kendiliğinden yeniden TAM gönderilir — "bağlam koptuysa yeniden" kuralı.
|
|
12
|
+
2) ESKİ ARAÇ ÇIKTISI: Son N araç sonucu birebir; daha eskiler başlık + baş/son
|
|
13
|
+
önizleme + "gerekirse tekrar çalıştır/oku" notu olarak sıkıştırılır.
|
|
14
|
+
Her dosyanın EN SON read_file sonucu asla sıkıştırılmaz (dosya hafızası).
|
|
15
|
+
3) SKILL: Aynı skill gövdesi tekrarlıysa eski kopyalar stub'a iner; tek kopya tam.
|
|
16
|
+
4) DOKUNULMAZLAR: system, son tur birimleri, assistant mesajları, zaten
|
|
17
|
+
sıkıştırılmış içerik (idempotans) ve minBlockChars'tan küçük bloklar. */
|
|
18
|
+
|
|
19
|
+
const crypto = require('crypto');
|
|
20
|
+
const { estMsgTokens } = require('./tokens');
|
|
21
|
+
|
|
22
|
+
const DEFAULTS = {
|
|
23
|
+
minTotalTokens: 1500,
|
|
24
|
+
minBlockChars: 700,
|
|
25
|
+
minUserCompactChars: 12000,
|
|
26
|
+
keepRecentTools: 0,
|
|
27
|
+
keepRecentUnits: 4,
|
|
28
|
+
protectReadsWindow: 8,
|
|
29
|
+
previewHead: 700,
|
|
30
|
+
previewTail: 250,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const MARK = '[squeeze:';
|
|
34
|
+
|
|
35
|
+
const state = {
|
|
36
|
+
enabled: false,
|
|
37
|
+
stats: {
|
|
38
|
+
calls: 0,
|
|
39
|
+
changedCalls: 0,
|
|
40
|
+
savedTokens: 0,
|
|
41
|
+
lastBeforeTokens: 0,
|
|
42
|
+
lastAfterTokens: 0,
|
|
43
|
+
lastSavedTokens: 0,
|
|
44
|
+
deduped: 0,
|
|
45
|
+
compactedTools: 0,
|
|
46
|
+
compactedSkills: 0,
|
|
47
|
+
compactedUsers: 0,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function setEnabled(on) {
|
|
52
|
+
state.enabled = !!on;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isEnabled() {
|
|
56
|
+
return state.enabled;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resetStats() {
|
|
60
|
+
for (const k of Object.keys(state.stats)) state.stats[k] = 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getStats() {
|
|
64
|
+
const s = { ...state.stats };
|
|
65
|
+
s.lastPercent =
|
|
66
|
+
s.lastBeforeTokens > 0 ? Math.round((s.lastSavedTokens / s.lastBeforeTokens) * 100) : 0;
|
|
67
|
+
return s;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function _hash(s) {
|
|
71
|
+
try {
|
|
72
|
+
return crypto.createHash('sha1').update(s).digest('hex').slice(0, 16);
|
|
73
|
+
} catch {
|
|
74
|
+
return String(s.length) + ':' + String(s.charCodeAt(0) || 0);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function _isSqueezed(content) {
|
|
79
|
+
return typeof content === 'string' && content.includes(MARK);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* assistant(tool_calls) + ardışık tool sonuçlarını BÖLMEYEN birimler (indis) */
|
|
83
|
+
function _indexUnits(messages) {
|
|
84
|
+
const units = [];
|
|
85
|
+
let i = 0;
|
|
86
|
+
while (i < messages.length) {
|
|
87
|
+
const m = messages[i];
|
|
88
|
+
if (m && m.role === 'assistant' && m.tool_calls && m.tool_calls.length) {
|
|
89
|
+
const grp = [i];
|
|
90
|
+
i++;
|
|
91
|
+
while (i < messages.length && messages[i] && messages[i].role === 'tool') grp.push(i++);
|
|
92
|
+
units.push(grp);
|
|
93
|
+
} else {
|
|
94
|
+
units.push([i]);
|
|
95
|
+
i++;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return units;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* tool_call_id → {name, args} (assistant mesajlarındaki çağrılardan) */
|
|
102
|
+
function _toolCallMap(messages) {
|
|
103
|
+
const map = new Map();
|
|
104
|
+
for (const m of messages) {
|
|
105
|
+
if (!m || m.role !== 'assistant' || !Array.isArray(m.tool_calls)) continue;
|
|
106
|
+
for (const tc of m.tool_calls) {
|
|
107
|
+
if (!tc || !tc.id) continue;
|
|
108
|
+
let args = {};
|
|
109
|
+
try {
|
|
110
|
+
args = JSON.parse((tc.function && tc.function.arguments) || '{}');
|
|
111
|
+
} catch {}
|
|
112
|
+
map.set(String(tc.id), {
|
|
113
|
+
name: String((tc.function && tc.function.name) || ''),
|
|
114
|
+
args,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return map;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function _readTarget(args) {
|
|
122
|
+
if (!args || typeof args !== 'object') return '';
|
|
123
|
+
for (const k of ['file_path', 'path', 'file', 'filename', 'filePath', 'target']) {
|
|
124
|
+
if (args[k]) return String(args[k]);
|
|
125
|
+
}
|
|
126
|
+
return '';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function _pointerFor(role, name) {
|
|
130
|
+
if (role === 'tool') {
|
|
131
|
+
return (
|
|
132
|
+
MARK + ' bu araç çıktısı yukarıdaki' + (name ? " '" + name + "'" : '') +
|
|
133
|
+
' sonucuyla BİREBİR AYNI — tekrar gönderilmedi]'
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return MARK + ' bu içerik yukarıda TAM gönderildi — tekrar gönderilmedi]';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function _skillStub(raw) {
|
|
140
|
+
let name = '';
|
|
141
|
+
try {
|
|
142
|
+
const j = JSON.parse(raw);
|
|
143
|
+
name = String(j.name || j.skill || '');
|
|
144
|
+
} catch {}
|
|
145
|
+
return (
|
|
146
|
+
'{"ok":true,"name":"' + name.replace(/"/g, '') + '",' +
|
|
147
|
+
'"content":"' + MARK + ' bu skill gövdesi yukarıda tam yüklendi — tekrar gönderilmedi]"}'
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function _compactPreview(content, cfg, label, hint) {
|
|
152
|
+
const len = content.length;
|
|
153
|
+
const headLen = Math.min(cfg.previewHead, Math.max(260, Math.floor(len * 0.4)));
|
|
154
|
+
const tailLen = len - headLen > 400 ? Math.min(cfg.previewTail, Math.floor(len * 0.15)) : 0;
|
|
155
|
+
const head = content.slice(0, headLen);
|
|
156
|
+
const tail = tailLen > 0 ? content.slice(-tailLen) : '';
|
|
157
|
+
const note = '\n' + MARK + ' ' + label + ' sıkıştırıldı: ' + len + ' karakter → önizleme. ' + hint + ']\n';
|
|
158
|
+
return tail ? head + note + tail : head + note;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* messages: OpenAI-uyumlu mesaj dizisi. Dönüş: (gerekirse) yeni dizi.
|
|
162
|
+
Kapalıyken aynı referans döner; açıkken de değişiklik yoksa orijinal döner. */
|
|
163
|
+
function apply(messages, opts = {}) {
|
|
164
|
+
if (!state.enabled || !Array.isArray(messages) || messages.length < 2) return messages;
|
|
165
|
+
|
|
166
|
+
const cfg = { ...DEFAULTS, ...(opts.config || {}) };
|
|
167
|
+
const beforeTokens = messages.reduce((a, m) => a + estMsgTokens(m), 0);
|
|
168
|
+
state.stats.calls++;
|
|
169
|
+
state.stats.lastBeforeTokens = beforeTokens;
|
|
170
|
+
state.stats.lastAfterTokens = beforeTokens;
|
|
171
|
+
state.stats.lastSavedTokens = 0;
|
|
172
|
+
if (beforeTokens < cfg.minTotalTokens) return messages;
|
|
173
|
+
|
|
174
|
+
const out = messages.slice();
|
|
175
|
+
let touched = false;
|
|
176
|
+
const replace = (i, msg) => {
|
|
177
|
+
out[i] = msg;
|
|
178
|
+
touched = true;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const units = _indexUnits(messages);
|
|
182
|
+
const protectFrom = Math.max(1, units.length - cfg.keepRecentUnits);
|
|
183
|
+
const protectedIdx = new Set([0]);
|
|
184
|
+
for (let u = protectFrom; u < units.length; u++) {
|
|
185
|
+
for (const i of units[u]) protectedIdx.add(i);
|
|
186
|
+
}
|
|
187
|
+
/* son N araç sonucu birebir kalır */
|
|
188
|
+
const toolIdx = [];
|
|
189
|
+
for (let i = 0; i < messages.length; i++) {
|
|
190
|
+
if (messages[i] && messages[i].role === 'tool') toolIdx.push(i);
|
|
191
|
+
}
|
|
192
|
+
if (cfg.keepRecentTools > 0) {
|
|
193
|
+
for (const i of toolIdx.slice(-cfg.keepRecentTools)) protectedIdx.add(i);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const meta = _toolCallMap(messages);
|
|
197
|
+
const toolName = (i) => {
|
|
198
|
+
const m = messages[i];
|
|
199
|
+
const fromCall = m && m.tool_call_id ? meta.get(String(m.tool_call_id)) : null;
|
|
200
|
+
return String((m && m.name) || (fromCall && fromCall.name) || '');
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/* her dosyanın EN SON okuması, yakın penceredeyse korunur */
|
|
204
|
+
const protectReadsFrom =
|
|
205
|
+
toolIdx.length > cfg.protectReadsWindow
|
|
206
|
+
? toolIdx[toolIdx.length - cfg.protectReadsWindow]
|
|
207
|
+
: (toolIdx[0] ?? Infinity);
|
|
208
|
+
const latestRead = new Map();
|
|
209
|
+
for (const i of toolIdx) {
|
|
210
|
+
const m = messages[i];
|
|
211
|
+
if (!m || String(m.name || '') !== 'read_file') continue;
|
|
212
|
+
const info = m.tool_call_id ? meta.get(String(m.tool_call_id)) : null;
|
|
213
|
+
const target = _readTarget(info && info.args) || '__last__';
|
|
214
|
+
latestRead.set(target, i);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let deduped = 0;
|
|
218
|
+
let compactedTools = 0;
|
|
219
|
+
let compactedSkills = 0;
|
|
220
|
+
let compactedUsers = 0;
|
|
221
|
+
|
|
222
|
+
/* 1) SKILL gövde tekrarı: aynı skill'in eski kopyaları stub'a iner */
|
|
223
|
+
const skillSeen = new Set();
|
|
224
|
+
for (let i = 0; i < messages.length; i++) {
|
|
225
|
+
const m = messages[i];
|
|
226
|
+
if (!m || m.role !== 'tool' || toolName(i) !== 'skill') continue;
|
|
227
|
+
if (typeof m.content !== 'string' || _isSqueezed(m.content)) continue;
|
|
228
|
+
let key = '';
|
|
229
|
+
try {
|
|
230
|
+
const j = JSON.parse(m.content);
|
|
231
|
+
key = String(j.name || j.skill || '');
|
|
232
|
+
} catch {}
|
|
233
|
+
if (!key) key = _hash(m.content);
|
|
234
|
+
if (skillSeen.has(key)) {
|
|
235
|
+
replace(i, { ...m, content: _skillStub(m.content) });
|
|
236
|
+
compactedSkills++;
|
|
237
|
+
} else {
|
|
238
|
+
skillSeen.add(key);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* 2) GENEL tekrar bloğu: ilk kopya tam, sonrakiler işaretçi (korumalılar hariç) */
|
|
243
|
+
const seen = new Map();
|
|
244
|
+
for (let i = 0; i < messages.length; i++) {
|
|
245
|
+
const m = messages[i];
|
|
246
|
+
if (!m || m.role === 'system' || m.role === 'assistant') continue;
|
|
247
|
+
if (typeof m.content !== 'string') continue;
|
|
248
|
+
if (m.content.length < cfg.minBlockChars || _isSqueezed(m.content)) continue;
|
|
249
|
+
const role = m.role === 'tool' ? 'tool' : 'user';
|
|
250
|
+
const key = role + ':' + _hash(m.content);
|
|
251
|
+
if (seen.has(key)) {
|
|
252
|
+
/* birebir aynı içerik: en son mesaj (güncel tur) hariç her yerde
|
|
253
|
+
işaretçiye inebilir — içerik zaten aynı bağlamda tam duruyor */
|
|
254
|
+
if (i === messages.length - 1) continue;
|
|
255
|
+
replace(i, { ...out[i], content: _pointerFor(role, toolName(i)) });
|
|
256
|
+
deduped++;
|
|
257
|
+
} else {
|
|
258
|
+
seen.set(key, i);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* 3) ESKİ ARAÇ ÇIKTISI: başlık + baş/son önizleme */
|
|
263
|
+
for (const i of toolIdx) {
|
|
264
|
+
if (protectedIdx.has(i)) continue;
|
|
265
|
+
const m = out[i];
|
|
266
|
+
if (!m || typeof m.content !== 'string' || _isSqueezed(m.content)) continue;
|
|
267
|
+
if (m.content.length < cfg.minBlockChars) continue;
|
|
268
|
+
const name = toolName(i);
|
|
269
|
+
if (name === 'skill') continue;
|
|
270
|
+
if (name === 'read_file') {
|
|
271
|
+
const info = m.tool_call_id ? meta.get(String(m.tool_call_id)) : null;
|
|
272
|
+
const target = _readTarget(info && info.args) || '__last__';
|
|
273
|
+
if (latestRead.get(target) === i && i >= protectReadsFrom) continue;
|
|
274
|
+
}
|
|
275
|
+
const hint =
|
|
276
|
+
name === 'read_file'
|
|
277
|
+
? 'Tam içerik diskte duruyor; gerekirse read_file ile tekrar oku'
|
|
278
|
+
: 'Gerekirse ' + (name || 'araç') + ' ile tekrar çalıştır';
|
|
279
|
+
replace(i, { ...m, content: _compactPreview(m.content, cfg, "'" + (name || 'araç') + "'", hint) });
|
|
280
|
+
compactedTools++;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/* 4) ESKİ BÜYÜK KULLANICI İÇERİĞİ (dosya eki vb.): baş/son önizleme */
|
|
284
|
+
for (let i = 0; i < messages.length; i++) {
|
|
285
|
+
const m = messages[i];
|
|
286
|
+
if (!m || m.role !== 'user' || protectedIdx.has(i) || i === 0) continue;
|
|
287
|
+
if (typeof m.content !== 'string' || _isSqueezed(m.content)) continue;
|
|
288
|
+
if (m.content.length < cfg.minUserCompactChars) continue;
|
|
289
|
+
if (m.content.startsWith('[ÖNCEKİ KONUŞMANIN ÖZETİ')) continue;
|
|
290
|
+
replace(i, {
|
|
291
|
+
...m,
|
|
292
|
+
content: _compactPreview(m.content, cfg, 'kullanıcı içeriği', 'Tam içerik oturum kaydında duruyor'),
|
|
293
|
+
});
|
|
294
|
+
compactedUsers++;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!touched) return messages;
|
|
298
|
+
|
|
299
|
+
const afterTokens = out.reduce((a, m) => a + estMsgTokens(m), 0);
|
|
300
|
+
const saved = Math.max(0, beforeTokens - afterTokens);
|
|
301
|
+
state.stats.lastAfterTokens = afterTokens;
|
|
302
|
+
state.stats.lastSavedTokens = saved;
|
|
303
|
+
state.stats.savedTokens += saved;
|
|
304
|
+
state.stats.deduped += deduped;
|
|
305
|
+
state.stats.compactedTools += compactedTools;
|
|
306
|
+
state.stats.compactedSkills += compactedSkills;
|
|
307
|
+
state.stats.compactedUsers += compactedUsers;
|
|
308
|
+
if (saved > 0) state.stats.changedCalls++;
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
module.exports = { apply, setEnabled, isEnabled, getStats, resetStats, MARK };
|
package/src/main.js
CHANGED
|
@@ -28,7 +28,7 @@ const bus = require('./agent/bus');
|
|
|
28
28
|
const computeruse = require('./agent/computeruse');
|
|
29
29
|
const fsguard = require('./agent/fsguard');
|
|
30
30
|
const log = require('./agent/logger');
|
|
31
|
-
const
|
|
31
|
+
const squeeze = require('./agent/squeeze');
|
|
32
32
|
const QRCode = require('qrcode'); /* Expo Go QR (bc-expurl) — whatsapp ile aynı paket */
|
|
33
33
|
|
|
34
34
|
/* Renderer'a sır gönderirken kullanılan maske; kaydederken aynen geri gelirse
|
|
@@ -317,6 +317,7 @@ let waHistory = new Map(); // jid -> [sid,...] bu sohbete ait tüm oturumlar
|
|
|
317
317
|
let waBcMode = new Set(); // jid -> BeastCode modu AKTİF (WhatsApp'tan uzaktan kodlama)
|
|
318
318
|
let waJidPn = new Map(); // jid -> gerçek telefon numarası (LID fallback için)
|
|
319
319
|
let waLastActiveJid = ''; // en son mesaj gelen WA sohbeti — kanal tek oturumunda cevap/dosya hedefi
|
|
320
|
+
let desktopActiveSid = ''; // masaüstü UI'da AÇIK olan oturum — proaktif not buraya işlenir
|
|
320
321
|
const WA_HISTORY_CAP = 20;
|
|
321
322
|
let tg = null;
|
|
322
323
|
let tgChats = new Map(); // telegram chatId -> aktif session id (hepsi KANAL TEK OTURUMUNA bağlı)
|
|
@@ -3465,29 +3466,9 @@ function reloadBackend() {
|
|
|
3465
3466
|
süreç koşucusu (sandbox:run IPC ile aynı makine). */
|
|
3466
3467
|
engine.sbRunHook = sbRunStartManaged;
|
|
3467
3468
|
|
|
3468
|
-
/*
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
const seen = new Map();
|
|
3472
|
-
for (const c of (engine && engine.cfg && engine.cfg.chain) || []) {
|
|
3473
|
-
if (!c || !c.providerId || !c.url || seen.has(c.providerId)) continue;
|
|
3474
|
-
seen.set(c.providerId, {
|
|
3475
|
-
id: String(c.providerId),
|
|
3476
|
-
name: String(c.providerName || c.providerId),
|
|
3477
|
-
baseUrl: String(c.url).replace(/\/chat\/completions$/, ''),
|
|
3478
|
-
});
|
|
3479
|
-
}
|
|
3480
|
-
return [...seen.values()];
|
|
3481
|
-
});
|
|
3482
|
-
headroom.setStatusSink((st) => {
|
|
3483
|
-
try {
|
|
3484
|
-
if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'headroom-status', status: st });
|
|
3485
|
-
} catch {}
|
|
3486
|
-
});
|
|
3487
|
-
/* ayarlardan açık kaldıysa: uygulama açılışında arka planda başlat */
|
|
3488
|
-
if (settings.headroom && settings.headroom.enabled) {
|
|
3489
|
-
setTimeout(() => { headroom.setEnabled(true).catch(() => {}); }, 4000);
|
|
3490
|
-
}
|
|
3469
|
+
/* SQUEEZE (opsiyonel token sıkıştırma): varsayılan KAPALI; ayar açıksa
|
|
3470
|
+
her LLM isteğinin kopyası gönderimden önce sıkıştırılır. */
|
|
3471
|
+
squeeze.setEnabled(!!(settings.squeeze && settings.squeeze.enabled));
|
|
3491
3472
|
return engine.publicState();
|
|
3492
3473
|
}
|
|
3493
3474
|
|
|
@@ -3718,7 +3699,6 @@ app.whenReady().then(() => {
|
|
|
3718
3699
|
flushBrowserStorage(); // x.com/google oturumları (cookies) diske yazılsın
|
|
3719
3700
|
try { toolsMod.disposeShellSessions(); } catch {} // kalıcı shell oturumlarını kapat
|
|
3720
3701
|
try { require('./agent/mcp').stopAll(); } catch {} // MCP server süreçlerini kapat
|
|
3721
|
-
try { headroom.stopAll(); } catch {} // headroom proxy süreçlerini kapat
|
|
3722
3702
|
});
|
|
3723
3703
|
|
|
3724
3704
|
if (process.argv.includes('--smoke')) {
|
|
@@ -5102,11 +5082,34 @@ ipcMain.handle('sessions:create', () => {
|
|
|
5102
5082
|
return v;
|
|
5103
5083
|
});
|
|
5104
5084
|
ipcMain.handle('sessions:open', (_e, id) => engine.openSession(id));
|
|
5105
|
-
ipcMain.handle('sessions:delete', (_e, id) =>
|
|
5085
|
+
ipcMain.handle('sessions:delete', (_e, id) => {
|
|
5086
|
+
const sid = String(id || '');
|
|
5087
|
+
if (sid && sid === desktopActiveSid) desktopActiveSid = '';
|
|
5088
|
+
return engine.deleteSession(id);
|
|
5089
|
+
});
|
|
5090
|
+
/* masaüstü UI hangi sohbeti AÇIK tutuyorsa main'e bildirir: empati loop
|
|
5091
|
+
proaktif notu rastgele bir oturuma değil, kullanıcının gördüğü sohbete
|
|
5092
|
+
işler; meşgulse enjeksiyon kuyruğa girer, tur bitince geçmişe düşer */
|
|
5093
|
+
ipcMain.handle('sessions:active', (_e, id) => {
|
|
5094
|
+
const sid = String(id || '');
|
|
5095
|
+
if (!sid) {
|
|
5096
|
+
desktopActiveSid = '';
|
|
5097
|
+
return true;
|
|
5098
|
+
}
|
|
5099
|
+
try {
|
|
5100
|
+
if (sessionFileAlive(sid)) desktopActiveSid = sid;
|
|
5101
|
+
} catch {}
|
|
5102
|
+
return true;
|
|
5103
|
+
});
|
|
5106
5104
|
|
|
5107
5105
|
ipcMain.handle('agent:send', (_e, { sessionId, text }) => {
|
|
5108
5106
|
const raw = text && typeof text === 'object' ? String(text.text || '') : String(text ?? '');
|
|
5109
5107
|
const t = raw.trim();
|
|
5108
|
+
/* yazan sohbet = kullanıcının gördüğü sohbet: proaktif enjeksiyon hedefi */
|
|
5109
|
+
try {
|
|
5110
|
+
const asid = String(sessionId || '');
|
|
5111
|
+
if (asid && sessionFileAlive(asid)) desktopActiveSid = asid;
|
|
5112
|
+
} catch {}
|
|
5110
5113
|
/* BOT HAFIZA GARANTİSİ: aktif bot bir MÜŞTERİ botuysa, botId'siz (eskiden
|
|
5111
5114
|
kalma) oturumlar bu bota bağlanır — bot konuşması asla Beast'in global
|
|
5112
5115
|
hafızasıyla (SOUL/USER/MEMORY) yürümez. Var olan botId asla üstüne yazılmaz. */
|
|
@@ -6673,44 +6676,24 @@ ipcMain.handle('install:status', async () => {
|
|
|
6673
6676
|
/* 8) Edge TTS (bulut) */
|
|
6674
6677
|
rows.push({ id: 'edge', name: 'Edge TTS — seslendirme', state: 'cloud', detail: 'bulut — kurulum gerekmez' });
|
|
6675
6678
|
|
|
6676
|
-
/* 9) Headroom CLI — arka plan paket kurulumu burada görünür;
|
|
6677
|
-
aç/kapat Token Sıkıştırma sekmesinde */
|
|
6678
|
-
{
|
|
6679
|
-
const st = headroom.probe();
|
|
6680
|
-
let state = 'optional';
|
|
6681
|
-
if (st.installed) state = 'ok';
|
|
6682
|
-
else if (st.installing) state = 'loading';
|
|
6683
|
-
if (st.failed) state = 'failed';
|
|
6684
|
-
rows.push({
|
|
6685
|
-
id: 'headroom',
|
|
6686
|
-
name: 'Headroom CLI — token sıkıştırma motoru',
|
|
6687
|
-
state,
|
|
6688
|
-
detail: st.installing
|
|
6689
|
-
? 'arka planda kuruluyor — uv tool install headroom-ai[proxy]'
|
|
6690
|
-
: st.failed
|
|
6691
|
-
? (st.error || 'kurulum başarısız')
|
|
6692
|
-
: st.installed
|
|
6693
|
-
? 'kurulu — aç/kapat: Token Sıkıştırma sekmesi'
|
|
6694
|
-
: 'kurulu değil — Token Sıkıştırma sekmesinden açınca otomatik kurulur',
|
|
6695
|
-
...(st.installing ? pctFields('headroom') : {}),
|
|
6696
|
-
});
|
|
6697
|
-
}
|
|
6698
|
-
|
|
6699
6679
|
return rows;
|
|
6700
6680
|
});
|
|
6701
6681
|
|
|
6702
6682
|
/* cua-driver kurulumunu şimdi başlat (Kurulum sekmesi otomatiği + elle tetikleme) */
|
|
6703
6683
|
ipcMain.handle('cua:install', () => require('./agent/computeruse').autoInstall());
|
|
6704
6684
|
|
|
6705
|
-
/*
|
|
6706
|
-
ipcMain.handle('
|
|
6707
|
-
settings.
|
|
6685
|
+
/* SQUEEZE (kendi token sıkıştırıcımız): aç/kapa + durum + istatistik */
|
|
6686
|
+
ipcMain.handle('squeeze:toggle', (_e, on) => {
|
|
6687
|
+
settings.squeeze = { ...(settings.squeeze || {}), enabled: !!on };
|
|
6708
6688
|
saveSettings();
|
|
6709
|
-
|
|
6710
|
-
return
|
|
6689
|
+
squeeze.setEnabled(!!on);
|
|
6690
|
+
return { ok: true, enabled: !!on };
|
|
6691
|
+
});
|
|
6692
|
+
ipcMain.handle('squeeze:status', () => ({ enabled: squeeze.isEnabled(), stats: squeeze.getStats() }));
|
|
6693
|
+
ipcMain.handle('squeeze:reset', () => {
|
|
6694
|
+
squeeze.resetStats();
|
|
6695
|
+
return { ok: true, stats: squeeze.getStats() };
|
|
6711
6696
|
});
|
|
6712
|
-
ipcMain.handle('headroom:status', () => headroom.probe());
|
|
6713
|
-
ipcMain.handle('headroom:stats', () => headroom.stats());
|
|
6714
6697
|
|
|
6715
6698
|
/* ANDROID EMÜLATÖR: kaldırıldı — mobil önizleme tunnel (telefon QR) yoluyla yapılır */
|
|
6716
6699
|
|
|
@@ -7251,10 +7234,18 @@ function ensureDcSession(channelId) {
|
|
|
7251
7234
|
return dcSingleSid;
|
|
7252
7235
|
}
|
|
7253
7236
|
|
|
7254
|
-
/* Masaüstü
|
|
7255
|
-
|
|
7237
|
+
/* Masaüstü hedefi: kullanıcının AÇIK sohbeti önceliklidir — proaktif not
|
|
7238
|
+
nereye bakıyorsa oraya işlenir; meşgulse empatiInjectToSession kuyruğa alır.
|
|
7239
|
+
Açık sohbet bilinmiyorsa en güncel meşgul olmayan sohbet (kanal oturumları
|
|
7240
|
+
hariç — onlara kanal yoluyla zaten enjekte edildi); o da yoksa yeni oturum. */
|
|
7241
|
+
function empatiDesktopSid(exclude) {
|
|
7242
|
+
const skip = exclude instanceof Set ? exclude : null;
|
|
7243
|
+
try {
|
|
7244
|
+
if (desktopActiveSid && sessionFileAlive(desktopActiveSid)) return String(desktopActiveSid);
|
|
7245
|
+
} catch {}
|
|
7256
7246
|
try {
|
|
7257
7247
|
for (const v of engine.listSessions()) {
|
|
7248
|
+
if (skip && skip.has(String(v.id))) continue;
|
|
7258
7249
|
if (!engine.isBusy(v.id)) return String(v.id);
|
|
7259
7250
|
}
|
|
7260
7251
|
} catch {}
|
|
@@ -7275,14 +7266,17 @@ function empatiNotify(text, ev) {
|
|
|
7275
7266
|
const outWa = PROACTIVE_MARK + '\n' + text + (srcWa ? '\n' + srcWa : '');
|
|
7276
7267
|
const inject = empatiInjectText(ev, out);
|
|
7277
7268
|
const senders = [];
|
|
7269
|
+
const channelSids = new Set(); /* bu tur enjeksiyon ALAN kanal oturumları */
|
|
7278
7270
|
const tryWa = () => {
|
|
7279
7271
|
try {
|
|
7280
7272
|
const own = waOwnerNum();
|
|
7281
7273
|
if (own && wa && wa.connected) {
|
|
7282
7274
|
const jid = own + '@s.whatsapp.net';
|
|
7275
|
+
const sid = ensureWaSession(jid);
|
|
7276
|
+
channelSids.add(String(sid));
|
|
7283
7277
|
senders.push(() =>
|
|
7284
7278
|
Promise.resolve(sendWaSafe(jid, outWa))
|
|
7285
|
-
.then(() => empatiInjectToSession(
|
|
7279
|
+
.then(() => empatiInjectToSession(sid, inject))
|
|
7286
7280
|
.catch(() => {})
|
|
7287
7281
|
);
|
|
7288
7282
|
}
|
|
@@ -7292,9 +7286,11 @@ function empatiNotify(text, ev) {
|
|
|
7292
7286
|
try {
|
|
7293
7287
|
if (tg && tg.connected) {
|
|
7294
7288
|
for (const id of tgOwnerIds()) {
|
|
7289
|
+
const sid = ensureTgSession(String(id));
|
|
7290
|
+
channelSids.add(String(sid));
|
|
7295
7291
|
senders.push(() =>
|
|
7296
7292
|
Promise.resolve(sendTgSafe(id, outPlain))
|
|
7297
|
-
.then(() => empatiInjectToSession(
|
|
7293
|
+
.then(() => empatiInjectToSession(sid, inject))
|
|
7298
7294
|
.catch(() => {})
|
|
7299
7295
|
);
|
|
7300
7296
|
}
|
|
@@ -7306,9 +7302,11 @@ function empatiNotify(text, ev) {
|
|
|
7306
7302
|
if (dc && dc.connected) {
|
|
7307
7303
|
const outDc = out.replace('🫡 *Beast proaktif:*', '🫡 **Beast proaktif:**');
|
|
7308
7304
|
for (const id of dcOwnerIds()) {
|
|
7305
|
+
const sid = ensureDcSession(String(id));
|
|
7306
|
+
channelSids.add(String(sid));
|
|
7309
7307
|
senders.push(() =>
|
|
7310
7308
|
Promise.resolve(sendDcSafe(id, outDc))
|
|
7311
|
-
.then(() => empatiInjectToSession(
|
|
7309
|
+
.then(() => empatiInjectToSession(sid, inject))
|
|
7312
7310
|
.catch(() => {})
|
|
7313
7311
|
);
|
|
7314
7312
|
}
|
|
@@ -7330,8 +7328,9 @@ function empatiNotify(text, ev) {
|
|
|
7330
7328
|
gider (kanal kullanıcısı ham BAĞLAM bloğunu görmez, ajan görür). */
|
|
7331
7329
|
try {
|
|
7332
7330
|
if (win && !win.isDestroyed()) {
|
|
7333
|
-
const dsid = empatiDesktopSid();
|
|
7334
|
-
|
|
7331
|
+
const dsid = String(empatiDesktopSid(channelSids) || '');
|
|
7332
|
+
/* aynı oturuma kanal yoluyla zaten enjekte edildiyse TEKRAR yazma */
|
|
7333
|
+
if (dsid && !channelSids.has(dsid)) empatiInjectToSession(dsid, out);
|
|
7335
7334
|
win.webContents.send('agent:event', { type: 'proactive', sessionId: dsid, id: ev.id, level: ev.level, title: ev.title, text: out });
|
|
7336
7335
|
}
|
|
7337
7336
|
} catch {}
|
|
@@ -11083,7 +11082,6 @@ ipcMain.handle('custom:set', (_e, list) => {
|
|
|
11083
11082
|
});
|
|
11084
11083
|
saveSettings();
|
|
11085
11084
|
engine.setCustomProviders(settings.customProviders);
|
|
11086
|
-
headroom.syncProviders(); /* headroom açıksa proxy setini yeni listeye uydur */
|
|
11087
11085
|
return engine.publicState();
|
|
11088
11086
|
});
|
|
11089
11087
|
|
package/src/preload.js
CHANGED
|
@@ -10,6 +10,7 @@ contextBridge.exposeInMainWorld('beast', {
|
|
|
10
10
|
listSessions: () => ipcRenderer.invoke('sessions:list'),
|
|
11
11
|
createSession: () => ipcRenderer.invoke('sessions:create'),
|
|
12
12
|
openSession: (id) => ipcRenderer.invoke('sessions:open', id),
|
|
13
|
+
setActiveSession: (id) => ipcRenderer.invoke('sessions:active', id),
|
|
13
14
|
deleteSession: (id) => ipcRenderer.invoke('sessions:delete', id),
|
|
14
15
|
send: (sessionId, text) => ipcRenderer.invoke('agent:send', { sessionId, text }),
|
|
15
16
|
interrupt: (sessionId, reason) => ipcRenderer.invoke('agent:interrupt', sessionId, reason),
|
|
@@ -260,9 +261,9 @@ contextBridge.exposeInMainWorld('beast', {
|
|
|
260
261
|
idePreviewUrl: (url) => ipcRenderer.invoke('ide:previewUrl', url),
|
|
261
262
|
ideProjInfo: () => ipcRenderer.invoke('ide:projinfo'),
|
|
262
263
|
ideExpQr: () => ipcRenderer.invoke('ide:expqr'),
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
squeezeToggle: (on) => ipcRenderer.invoke('squeeze:toggle', !!on),
|
|
265
|
+
squeezeStatus: () => ipcRenderer.invoke('squeeze:status'),
|
|
266
|
+
squeezeReset: () => ipcRenderer.invoke('squeeze:reset'),
|
|
266
267
|
agentsClearAll: () => ipcRenderer.invoke('agents:clearAll'),
|
|
267
268
|
ideDelete: (rel) => ipcRenderer.invoke('ide:delete', rel),
|
|
268
269
|
onWaEvent: (cb) => ipcRenderer.on('wa:event', (_e, ev) => cb(ev)),
|
package/src/renderer/i18n.js
CHANGED
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
set_title: 'AYARLAR',
|
|
67
67
|
tab_lang: 'Dil',
|
|
68
68
|
lang_sub: 'Arayüz dili — uygulama dili ve sesli komut dili buradan değişir',
|
|
69
|
-
|
|
69
|
+
tab_squeeze: 'Squeeze',
|
|
70
70
|
tab_provider: 'Provider',
|
|
71
71
|
tab_fallout: 'Fallout',
|
|
72
72
|
tab_memory: 'Memory',
|
|
@@ -787,7 +787,7 @@
|
|
|
787
787
|
set_title: 'SETTINGS',
|
|
788
788
|
tab_lang: 'Language',
|
|
789
789
|
lang_sub: 'Interface language — app UI and voice command language',
|
|
790
|
-
|
|
790
|
+
tab_squeeze: 'Squeeze',
|
|
791
791
|
tab_provider: 'Provider',
|
|
792
792
|
tab_fallout: 'Fallout',
|
|
793
793
|
tab_memory: 'Memory',
|
package/src/renderer/index.html
CHANGED
|
@@ -511,7 +511,7 @@
|
|
|
511
511
|
<button class="tab" data-tab="cron" data-i18n="tab_cron">Cron</button>
|
|
512
512
|
<button class="tab" data-tab="agenda" data-i18n="tab_agenda">Ajanda</button>
|
|
513
513
|
<button class="tab" data-tab="usage" data-i18n="tab_usage">Maliyet · Limit</button>
|
|
514
|
-
<button class="tab" data-tab="
|
|
514
|
+
<button class="tab" data-tab="squeeze" data-i18n="tab_squeeze">Squeeze</button>
|
|
515
515
|
<button class="tab" data-tab="logs" data-i18n="tab_logs">Log</button>
|
|
516
516
|
<button class="tab" data-tab="dash" data-i18n="tab_dash">Dashboard</button>
|
|
517
517
|
<button class="tab" data-tab="sec" data-i18n="tab_security">Güvenlik</button>
|
|
@@ -537,7 +537,7 @@
|
|
|
537
537
|
<div id="tab-events" class="pane" hidden></div>
|
|
538
538
|
<div id="tab-empati" class="pane" hidden></div>
|
|
539
539
|
<div id="tab-usage" class="pane" hidden></div>
|
|
540
|
-
<div id="tab-
|
|
540
|
+
<div id="tab-squeeze" class="pane" hidden></div>
|
|
541
541
|
<div id="tab-logs" class="pane" hidden></div>
|
|
542
542
|
<div id="tab-dash" class="pane" hidden></div>
|
|
543
543
|
<div id="tab-sec" class="pane" hidden></div>
|
package/src/renderer/renderer.js
CHANGED
|
@@ -1077,6 +1077,7 @@ setInterval(() => {
|
|
|
1077
1077
|
|
|
1078
1078
|
async function openSession(id) {
|
|
1079
1079
|
activeId = id;
|
|
1080
|
+
try { beast.setActiveSession(id); } catch {}
|
|
1080
1081
|
streamEl = null;
|
|
1081
1082
|
const s = await beast.openSession(id);
|
|
1082
1083
|
els.msgs.innerHTML = '';
|
|
@@ -1213,7 +1214,6 @@ function addFiles(files, mode) {
|
|
|
1213
1214
|
/* ---------------- settings ---------------- */
|
|
1214
1215
|
|
|
1215
1216
|
let setTab = 'lang'; /* ayarlar artık DİL sekmesiyle açılır */
|
|
1216
|
-
let hrPaneTimer = null; /* headroom-status → Kurulum sekmesi debounce tazeleme */
|
|
1217
1217
|
|
|
1218
1218
|
/* DİL sekmesi: arayüz dili seçimi (eski sağ-alt dil ikonunun yerine) */
|
|
1219
1219
|
function renderLangPane() {
|
|
@@ -1280,7 +1280,7 @@ async function renderActiveSettingsTab() {
|
|
|
1280
1280
|
case 'cron': await openCron(); break;
|
|
1281
1281
|
case 'agenda': await renderAgendaPane(); break;
|
|
1282
1282
|
case 'usage': await renderUsagePane(); break;
|
|
1283
|
-
case '
|
|
1283
|
+
case 'squeeze': await renderSqueezePane(); break;
|
|
1284
1284
|
case 'agents': await refreshAgentsPane(); break;
|
|
1285
1285
|
case 'logs': await renderLogPane(); break;
|
|
1286
1286
|
case 'dash': await renderDashboardPane(); break;
|
|
@@ -1364,7 +1364,7 @@ function switchTab(name) {
|
|
|
1364
1364
|
document.querySelectorAll('#setTabs .tab').forEach((b) =>
|
|
1365
1365
|
b.classList.toggle('active', b.dataset.tab === name)
|
|
1366
1366
|
);
|
|
1367
|
-
for (const p of ['lang', 'provider', 'fallout', 'skills', 'agents', 'tts', 'install', 'email', 'integrations', 'websearch', 'mcp', 'events', 'empati', 'cron', 'agenda', 'usage', '
|
|
1367
|
+
for (const p of ['lang', 'provider', 'fallout', 'skills', 'agents', 'tts', 'install', 'email', 'integrations', 'websearch', 'mcp', 'events', 'empati', 'cron', 'agenda', 'usage', 'squeeze', 'logs', 'dash', 'sec', 'update']) {
|
|
1368
1368
|
const el = $('#tab-' + p);
|
|
1369
1369
|
if (el) el.hidden = p !== name; // guard: eksik pane tüm sekmeleri kilitlemesin
|
|
1370
1370
|
}
|
|
@@ -1374,7 +1374,7 @@ function switchTab(name) {
|
|
|
1374
1374
|
if (name === 'email') renderEmailPane();
|
|
1375
1375
|
if (name === 'usage') renderUsagePane();
|
|
1376
1376
|
if (name === 'install') renderInstallPane();
|
|
1377
|
-
if (name === '
|
|
1377
|
+
if (name === 'squeeze') renderSqueezePane();
|
|
1378
1378
|
if (name === 'events') renderEventsPane();
|
|
1379
1379
|
if (name === 'empati') renderEmpatiPane();
|
|
1380
1380
|
if (name === 'logs') renderLogPane();
|
|
@@ -2332,91 +2332,90 @@ function ttsFlushTail() {
|
|
|
2332
2332
|
/* ---------------- KURULUM SEKMESİ: bileşenler OTOMATİK kurulur ----------------
|
|
2333
2333
|
Eksik/kısmen inmiş bileşen sekme açılınca kendiliğinden indirilir; kullanıcı
|
|
2334
2334
|
hiçbir düğmeye basmaz. Durum canlı güncellenir (4 sn'de bir). */
|
|
2335
|
-
/* ----------
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2335
|
+
/* ---------- SQUEEZE (kendi token sıkıştırıcımız) sekmesi ----------
|
|
2336
|
+
Varsayılan KAPALI. Açıkken her LLM isteğinin kopyası gönderimden önce
|
|
2337
|
+
sıkıştırılır: tekrar eden büyük bloklar bağlamda bir kez gider, eski araç
|
|
2338
|
+
çıktıları başlık + önizlemeye iner. Oturum kaydı değişmez. */
|
|
2339
|
+
let sqStatsTimer = null;
|
|
2340
|
+
|
|
2341
|
+
async function renderSqueezePane() {
|
|
2342
|
+
const pane = $('#tab-squeeze');
|
|
2342
2343
|
if (!pane) return;
|
|
2343
|
-
const st = await beast.
|
|
2344
|
-
|
|
2344
|
+
const st = await beast.squeezeStatus().catch(() => null);
|
|
2345
|
+
const on = !!(st && st.enabled);
|
|
2346
|
+
const s = (st && st.stats) || {};
|
|
2345
2347
|
const fmtN = (n) => new Intl.NumberFormat('tr-TR').format(Math.round(Number(n) || 0));
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
else if (st.installed) stateTxt = '⚪ kapalı — kurulu, aşağıdaki düğmeyle etkinleştir';
|
|
2355
|
-
else stateTxt = '⚪ kapalı — açınca uv ile otomatik kurulur (birkaç dakika)';
|
|
2356
|
-
stHtml = '<div class="sub" style="margin-top:10px;font-size:12px;text-align:center">' + escapeHtml(stateTxt) + '</div>';
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
const stats = await beast.headroomStats().catch(() => null);
|
|
2348
|
+
|
|
2349
|
+
const stHtml =
|
|
2350
|
+
'<div class="sub" style="margin-top:10px;font-size:12px;text-align:center">' +
|
|
2351
|
+
(on
|
|
2352
|
+
? '🟢 açık — her isteğin kopyası gönderimden önce sıkıştırılır; oturum kaydın değişmez'
|
|
2353
|
+
: '⚪ kapalı (varsayılan) — açınca yeni isteklerde uygulanır, geçmişe dokunmaz') +
|
|
2354
|
+
'</div>';
|
|
2355
|
+
|
|
2360
2356
|
let statsHtml = '';
|
|
2361
|
-
if (
|
|
2357
|
+
if (s.calls) {
|
|
2362
2358
|
statsHtml =
|
|
2363
2359
|
'<div class="divider"></div>' +
|
|
2364
|
-
'<h2 style="text-align:center">Tasarruf
|
|
2360
|
+
'<h2 style="text-align:center">Tasarruf</h2>' +
|
|
2365
2361
|
'<div style="display:flex;gap:24px;flex-wrap:wrap;margin-top:8px;justify-content:center">' +
|
|
2366
|
-
'<div><div style="font-size:20px;font-weight:800;color:var(--ok)">%' +
|
|
2367
|
-
'<div><div style="font-size:20px;font-weight:800">' +
|
|
2368
|
-
'<div><div style="font-size:20px;font-weight:800">' +
|
|
2369
|
-
'</div>'
|
|
2362
|
+
'<div title="Son istekte tahmini token tasarrufu"><div style="font-size:20px;font-weight:800;color:var(--ok)">%' + (s.lastPercent || 0) + '</div><div class="sub">son istekte sıkıştırma</div></div>' +
|
|
2363
|
+
'<div><div style="font-size:20px;font-weight:800">' + fmtN(s.savedTokens) + '</div><div class="sub">toplam kaydedilen token (tahmini)</div></div>' +
|
|
2364
|
+
'<div><div style="font-size:20px;font-weight:800">' + fmtN(s.changedCalls) + '</div><div class="sub">sıkıştırılan istek</div></div>' +
|
|
2365
|
+
'</div>' +
|
|
2366
|
+
'<div class="sub" style="text-align:center;margin-top:8px">tekrar bloğu: ' + fmtN(s.deduped) +
|
|
2367
|
+
' · eski araç çıktısı: ' + fmtN(s.compactedTools) +
|
|
2368
|
+
' · skill: ' + fmtN(s.compactedSkills) +
|
|
2369
|
+
' · kullanıcı içeriği: ' + fmtN(s.compactedUsers) + '</div>';
|
|
2370
2370
|
}
|
|
2371
2371
|
|
|
2372
|
-
/* AÇ/KAPAT düğmesi: ortalanmış + SVG ikonlu (play/pause — uygulama ikon stili) */
|
|
2373
|
-
const svgPlay =
|
|
2374
|
-
'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="6 3 20 12 6 21 6 3"/></svg>';
|
|
2375
|
-
const svgPause =
|
|
2376
|
-
'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/></svg>';
|
|
2377
2372
|
pane.innerHTML =
|
|
2378
|
-
'<h2 style="text-align:center">' + _t('
|
|
2379
|
-
'<div class="sub" style="text-align:center;max-width:620px;margin:0 auto">Headroom yerel proxy\u2019si — araç çıktıları, loglar ve JSON yükleri modele gitmeden sıkıştırılır; orijinali yerelde saklanır. Token faturasını %30-70 düşürür. Yalnız OpenAI-uyumlu sağlayıcı trafiğini etkiler; kapalıyken hiçbir şey değişmez.</div>' +
|
|
2373
|
+
'<h2 style="text-align:center">' + _t('tab_squeeze') + '</h2>' +
|
|
2380
2374
|
'<div style="display:flex;flex-direction:column;align-items:center;gap:10px;margin-top:18px">' +
|
|
2381
|
-
'<button id="
|
|
2382
|
-
'<span id="
|
|
2375
|
+
'<button id="sqToggle" class="btn" style="width:auto;padding:11px 16px;display:inline-flex;align-items:center;gap:8px;justify-content:center" title=""></button>' +
|
|
2376
|
+
'<span id="sqToggleMsg" class="sub" style="margin:0;text-align:center;max-width:560px"></span>' +
|
|
2383
2377
|
'</div>' +
|
|
2384
|
-
stHtml + statsHtml
|
|
2378
|
+
stHtml + statsHtml +
|
|
2379
|
+
(s.calls ? '<div style="text-align:center;margin-top:12px"><button id="sqReset" class="btn" style="width:auto;padding:6px 12px;font-size:12px">İstatistiği sıfırla</button></div>' : '');
|
|
2385
2380
|
|
|
2386
|
-
const
|
|
2381
|
+
const svgPlay =
|
|
2382
|
+
'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="6 3 20 12 6 21 6 3"/></svg>';
|
|
2383
|
+
const svgPause =
|
|
2384
|
+
'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/></svg>';
|
|
2385
|
+
const btn = $('#sqToggle');
|
|
2387
2386
|
if (btn) {
|
|
2388
|
-
const on = !!(st && st.enabled);
|
|
2389
2387
|
/* yalnız SVG ikon — anlamı durum satırı + tooltip taşır */
|
|
2390
2388
|
btn.innerHTML = on ? svgPause : svgPlay;
|
|
2391
|
-
btn.title = on ? '
|
|
2389
|
+
btn.title = on ? 'Squeeze\'i kapat' : 'Squeeze\'i aç';
|
|
2392
2390
|
btn.setAttribute('aria-label', btn.title);
|
|
2393
2391
|
btn.addEventListener('click', async () => {
|
|
2394
|
-
const msg = $('#
|
|
2392
|
+
const msg = $('#sqToggleMsg');
|
|
2395
2393
|
btn.disabled = true;
|
|
2396
2394
|
btn.innerHTML = '<span>…</span>';
|
|
2397
|
-
const r = await beast.
|
|
2395
|
+
const r = await beast.squeezeToggle(!on).catch(() => null);
|
|
2398
2396
|
if (msg) {
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
msg.textContent = r.error;
|
|
2403
|
-
} else {
|
|
2404
|
-
msg.textContent = !on ? 'Açıldı — proxy hazır olunca trafiği yönlendirir' : 'Kapatıldı — trafik sağlayıcıya düz gidiyor';
|
|
2405
|
-
}
|
|
2397
|
+
msg.textContent = r && r.ok
|
|
2398
|
+
? (!on ? 'Açıldı — sonraki istekler sıkıştırılarak gönderilir' : 'Kapatıldı — istekler olduğu gibi gider')
|
|
2399
|
+
: 'Ayar kaydedilemedi';
|
|
2406
2400
|
}
|
|
2407
|
-
setTimeout(() =>
|
|
2401
|
+
setTimeout(() => renderSqueezePane(), 400);
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
const rb = $('#sqReset');
|
|
2405
|
+
if (rb) {
|
|
2406
|
+
rb.addEventListener('click', async () => {
|
|
2407
|
+
rb.disabled = true;
|
|
2408
|
+
await beast.squeezeReset().catch(() => null);
|
|
2409
|
+
renderSqueezePane();
|
|
2408
2410
|
});
|
|
2409
2411
|
}
|
|
2410
2412
|
|
|
2411
2413
|
/* açıkken istatistikler canlı aksın */
|
|
2412
|
-
if (
|
|
2413
|
-
|
|
2414
|
-
if (pane.hidden) { clearInterval(
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
if (s2 && cur && cur.disabled) return; /* toggle işlemi sürüyor */
|
|
2418
|
-
await renderHeadroomPane();
|
|
2419
|
-
}, 6000);
|
|
2414
|
+
if (sqStatsTimer) clearInterval(sqStatsTimer);
|
|
2415
|
+
sqStatsTimer = setInterval(() => {
|
|
2416
|
+
if (pane.hidden) { clearInterval(sqStatsTimer); return; }
|
|
2417
|
+
renderSqueezePane();
|
|
2418
|
+
}, 5000);
|
|
2420
2419
|
}
|
|
2421
2420
|
|
|
2422
2421
|
async function renderInstallPane() {
|
|
@@ -2486,24 +2485,10 @@ async function renderInstallPane() {
|
|
|
2486
2485
|
'<div class="sub" style="font-size:11px">' + escapeHtml(r.detail || '') + (r.mb ? ' · ' + r.mb + ' MB' : '') + '</div>' +
|
|
2487
2486
|
bar(r) +
|
|
2488
2487
|
'</div>' +
|
|
2489
|
-
(r.canToggle
|
|
2490
|
-
? '<button class="btn hr-tgl" data-on="' + (r.toggleOn ? '1' : '0') + '" style="width:auto;padding:2px 12px;flex:none">' + (r.toggleOn ? 'KAPAT' : 'AÇ') + '</button>'
|
|
2491
|
-
: '') +
|
|
2492
2488
|
badge(r.state) +
|
|
2493
2489
|
pctLabel(r) +
|
|
2494
2490
|
'</div>'
|
|
2495
2491
|
)).join('');
|
|
2496
|
-
/* Headroom aç/kapa: toggle kurulum + proxy başlatmayı arka planda tetikler */
|
|
2497
|
-
el.querySelectorAll('.hr-tgl').forEach((b) => {
|
|
2498
|
-
b.addEventListener('click', async () => {
|
|
2499
|
-
const on = b.dataset.on !== '1';
|
|
2500
|
-
b.disabled = true;
|
|
2501
|
-
b.textContent = '…';
|
|
2502
|
-
const r = await beast.headroomToggle(on).catch(() => null);
|
|
2503
|
-
toast(r && r.error ? r.error : (r && r.installing ? 'Headroom kuruluyor — birkaç dakika sürer' : (on ? 'Headroom açıldı' : 'Headroom kapatıldı')));
|
|
2504
|
-
refresh();
|
|
2505
|
-
});
|
|
2506
|
-
});
|
|
2507
2492
|
};
|
|
2508
2493
|
|
|
2509
2494
|
const refresh = async () => {
|
|
@@ -5745,19 +5730,6 @@ function onEvent(ev) {
|
|
|
5745
5730
|
showPermissionCard(req, isBc);
|
|
5746
5731
|
return;
|
|
5747
5732
|
}
|
|
5748
|
-
/* Headroom durum değişimi → açık olan sekmeyi tazele (Token Sıkıştırma / Kurulum) */
|
|
5749
|
-
if (ev.type === 'headroom-status') {
|
|
5750
|
-
try {
|
|
5751
|
-
const iPane = document.getElementById('tab-install');
|
|
5752
|
-
const hPane = document.getElementById('tab-headroom');
|
|
5753
|
-
const open = (!iPane || iPane.hidden) && (!hPane || hPane.hidden) ? '' : (!hPane || hPane.hidden ? 'install' : 'headroom');
|
|
5754
|
-
if (open) {
|
|
5755
|
-
clearTimeout(hrPaneTimer);
|
|
5756
|
-
hrPaneTimer = setTimeout(() => { (open === 'headroom' ? renderHeadroomPane() : renderInstallPane()).catch(() => {}); }, 1500);
|
|
5757
|
-
}
|
|
5758
|
-
} catch {}
|
|
5759
|
-
return;
|
|
5760
|
-
}
|
|
5761
5733
|
/* EMPATİ LOOP: proaktif bildirim (masaüstü) + sekme canlı yenileme.
|
|
5762
5734
|
Toast'ta yalnız haber başlığı — link/markdown kalabalığı chat mesajında. */
|
|
5763
5735
|
if (ev.type === 'proactive') {
|
package/src/agent/headroom.js
DELETED
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/* ---------- BEAST × HEADROOM (OPSİYONEL token sıkıştırma) ----------
|
|
4
|
-
Ayarlardan (Kurulum sekmesi) açılıp kapanır; kapalıyken hiçbir şey yapmaz.
|
|
5
|
-
Açıkken: LLM istekleri yerel headroom proxy'sinden geçer — araç çıktıları,
|
|
6
|
-
loglar ve JSON yükleri modele gitmeden sıkıştırılır (testte %39-71 tasarruf).
|
|
7
|
-
Kurulum: uv tool install "headroom-ai[proxy]" (uv yoksa hata bildirir).
|
|
8
|
-
Her provider için AYRI proxy portu (base URL farkları için); llm.js
|
|
9
|
-
providerId → port haritasıyla yönlendirir, kapatınca düz bağlanır. */
|
|
10
|
-
|
|
11
|
-
const { spawn } = require('child_process');
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
const os = require('os');
|
|
15
|
-
const { emitInstallProgress } = require('./progressbus');
|
|
16
|
-
|
|
17
|
-
const PORT_BASE = 18787;
|
|
18
|
-
const MAX_PROXIES = 8;
|
|
19
|
-
|
|
20
|
-
const state = {
|
|
21
|
-
enabled: false,
|
|
22
|
-
installing: false,
|
|
23
|
-
installFailed: false,
|
|
24
|
-
installError: '',
|
|
25
|
-
starting: false,
|
|
26
|
-
proxies: new Map(), // providerId -> { proc, port, baseUrl, ready }
|
|
27
|
-
providerGetter: null, // () => [{ id, name, baseUrl }]
|
|
28
|
-
statusSink: null, // (probe) => void — main renderer'a akıtır
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
function _emit() {
|
|
32
|
-
try { if (state.statusSink) state.statusSink(probe()); } catch {}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function headroomExe() {
|
|
36
|
-
const local = path.join(os.homedir(), '.local', 'bin', 'headroom.exe');
|
|
37
|
-
if (fs.existsSync(local)) return local;
|
|
38
|
-
try {
|
|
39
|
-
require('child_process').execSync('where headroom', { timeout: 5000, stdio: 'pipe' });
|
|
40
|
-
return 'headroom';
|
|
41
|
-
} catch {}
|
|
42
|
-
return '';
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function uvExe() {
|
|
46
|
-
const local = path.join(os.homedir(), '.local', 'bin', 'uv.exe');
|
|
47
|
-
if (fs.existsSync(local)) return local;
|
|
48
|
-
try {
|
|
49
|
-
require('child_process').execSync('where uv', { timeout: 5000, stdio: 'pipe' });
|
|
50
|
-
return 'uv';
|
|
51
|
-
} catch {}
|
|
52
|
-
return '';
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function probe() {
|
|
56
|
-
return {
|
|
57
|
-
installed: !!headroomExe(),
|
|
58
|
-
installing: state.installing,
|
|
59
|
-
failed: state.installFailed,
|
|
60
|
-
error: state.installError,
|
|
61
|
-
enabled: state.enabled,
|
|
62
|
-
starting: state.starting,
|
|
63
|
-
proxies: [...state.proxies.values()].map((p) => ({ port: p.port, baseUrl: p.baseUrl, ready: p.ready })),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/* ---------- KURULUM (arka plan) ----------
|
|
68
|
-
uv tool install headroom-ai[proxy] — kendi izole Python 3.13 ortamını kurar;
|
|
69
|
-
sistemde Python gerekmez. uv yoksa pip fallback, o da yoksa elle yönlendirir. */
|
|
70
|
-
function autoInstall() {
|
|
71
|
-
if (state.installing || headroomExe()) return;
|
|
72
|
-
const uv = uvExe();
|
|
73
|
-
if (!uv) {
|
|
74
|
-
state.installFailed = true;
|
|
75
|
-
state.installError = 'uv bulunamadı — kur: winget install astral-sh.uv';
|
|
76
|
-
_emit();
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
state.installing = true;
|
|
80
|
-
state.installFailed = false;
|
|
81
|
-
state.installError = '';
|
|
82
|
-
_emit();
|
|
83
|
-
emitInstallProgress('headroom', { pct: 4 });
|
|
84
|
-
const proc = spawn(uv, ['tool', 'install', '--python', '3.13', 'headroom-ai[proxy]'], { windowsHide: true });
|
|
85
|
-
let buf = '';
|
|
86
|
-
let stage = 4;
|
|
87
|
-
const bump = () => {
|
|
88
|
-
stage = Math.min(92, stage + 7);
|
|
89
|
-
emitInstallProgress('headroom', { pct: stage });
|
|
90
|
-
};
|
|
91
|
-
const onData = (c) => {
|
|
92
|
-
const s = String(c);
|
|
93
|
-
if (/Download|Resolve|Installed|Prepared/i.test(s)) bump();
|
|
94
|
-
};
|
|
95
|
-
proc.stdout.on('data', onData);
|
|
96
|
-
proc.stderr.on('data', onData);
|
|
97
|
-
proc.on('close', (code) => {
|
|
98
|
-
state.installing = false;
|
|
99
|
-
if (code === 0 && headroomExe()) {
|
|
100
|
-
emitInstallProgress('headroom', { pct: 100 });
|
|
101
|
-
} else {
|
|
102
|
-
state.installFailed = true;
|
|
103
|
-
state.installError = 'kurulum başarısız — elle: uv tool install "headroom-ai[proxy]"';
|
|
104
|
-
}
|
|
105
|
-
_emit();
|
|
106
|
-
/* kurulum bitti ve açık kaldıysa proxy'leri hemen başlat */
|
|
107
|
-
if (state.enabled && headroomExe()) start().catch(() => {});
|
|
108
|
-
});
|
|
109
|
-
proc.on('error', () => {
|
|
110
|
-
state.installing = false;
|
|
111
|
-
state.installFailed = true;
|
|
112
|
-
state.installError = 'kurulum başlatılamadı (uv)';
|
|
113
|
-
_emit();
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/* ---------- PROXY SÜREÇLERİ ----------
|
|
118
|
-
Provider başına bir proxy; cache modunu BOZMAMAK için mode=token,
|
|
119
|
-
Beast'in tanımadığı headroom_retrieve aracı enjekte EDİLMESİN diye
|
|
120
|
-
no-ccr (geri-getirilemez sıkıştırma, en yüksek tasarruf) ve rate limit
|
|
121
|
-
kapalı (Beast'in kendi istek patlamalarına takılmasın). Beacon kapalı. */
|
|
122
|
-
function nextPort() {
|
|
123
|
-
const used = new Set([...state.proxies.values()].map((p) => p.port));
|
|
124
|
-
for (let i = 0; i < MAX_PROXIES; i++) {
|
|
125
|
-
const port = PORT_BASE + i;
|
|
126
|
-
if (!used.has(port)) return port;
|
|
127
|
-
}
|
|
128
|
-
return 0;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function spawnFor(provider) {
|
|
132
|
-
return new Promise((resolve) => {
|
|
133
|
-
const exe = headroomExe();
|
|
134
|
-
if (!exe) return resolve({ ok: false, error: 'headroom kurulu değil' });
|
|
135
|
-
const port = nextPort();
|
|
136
|
-
if (!port) return resolve({ ok: false, error: 'port kalmadı' });
|
|
137
|
-
let proc;
|
|
138
|
-
try {
|
|
139
|
-
proc = spawn(exe, [
|
|
140
|
-
'proxy', '--port', String(port), '--openai-api-url', provider.baseUrl,
|
|
141
|
-
'--mode', 'token', '--no-ccr', '--no-rate-limit',
|
|
142
|
-
], {
|
|
143
|
-
windowsHide: true,
|
|
144
|
-
env: { ...process.env, HEADROOM_BEACON: 'off', DO_NOT_TRACK: '1' },
|
|
145
|
-
});
|
|
146
|
-
} catch (e) {
|
|
147
|
-
return resolve({ ok: false, error: String((e && e.message) || e) });
|
|
148
|
-
}
|
|
149
|
-
const rec = { proc, port, baseUrl: provider.baseUrl, ready: false };
|
|
150
|
-
state.proxies.set(provider.id, rec);
|
|
151
|
-
_emit();
|
|
152
|
-
proc.on('exit', () => {
|
|
153
|
-
if (state.proxies.get(provider.id) === rec) state.proxies.delete(provider.id);
|
|
154
|
-
_emit();
|
|
155
|
-
});
|
|
156
|
-
const t0 = Date.now();
|
|
157
|
-
const poll = setInterval(async () => {
|
|
158
|
-
let ready = false;
|
|
159
|
-
try {
|
|
160
|
-
const r = await fetch('http://127.0.0.1:' + port + '/health', { signal: AbortSignal.timeout(2000) });
|
|
161
|
-
ready = !!r.ok;
|
|
162
|
-
} catch {}
|
|
163
|
-
if (ready) {
|
|
164
|
-
clearInterval(poll);
|
|
165
|
-
rec.ready = true;
|
|
166
|
-
_emit();
|
|
167
|
-
return resolve({ ok: true, port });
|
|
168
|
-
}
|
|
169
|
-
if (Date.now() - t0 > 90000) {
|
|
170
|
-
clearInterval(poll);
|
|
171
|
-
try { rec.proc.kill(); } catch {}
|
|
172
|
-
state.proxies.delete(provider.id);
|
|
173
|
-
_emit();
|
|
174
|
-
resolve({ ok: false, error: 'proxy zaman aşımı (90sn)' });
|
|
175
|
-
}
|
|
176
|
-
}, 1200);
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
async function start() {
|
|
181
|
-
const exe = headroomExe();
|
|
182
|
-
if (!exe) {
|
|
183
|
-
autoInstall();
|
|
184
|
-
return { ok: false, error: 'kuruluyor — birkaç dakika sonra hazır olur' };
|
|
185
|
-
}
|
|
186
|
-
state.starting = true;
|
|
187
|
-
_emit();
|
|
188
|
-
try {
|
|
189
|
-
const providers = (state.providerGetter ? state.providerGetter() : []).slice(0, MAX_PROXIES);
|
|
190
|
-
const routes = {};
|
|
191
|
-
/* önce eskileri temizle: listeden çıkan provider'ın proxy'sini kapat */
|
|
192
|
-
const keep = new Set(providers.map((p) => p.id));
|
|
193
|
-
for (const [id, rec] of [...state.proxies.entries()]) {
|
|
194
|
-
if (keep.has(id)) continue;
|
|
195
|
-
try { rec.proc.kill(); } catch {}
|
|
196
|
-
try { spawn('taskkill', ['/pid', String(rec.proc.pid), '/T', '/F'], { windowsHide: true }); } catch {}
|
|
197
|
-
state.proxies.delete(id);
|
|
198
|
-
}
|
|
199
|
-
for (const p of providers) {
|
|
200
|
-
if (!p.baseUrl || state.proxies.has(p.id)) continue;
|
|
201
|
-
const r = await spawnFor(p);
|
|
202
|
-
if (r.ok) routes[p.id] = r.port;
|
|
203
|
-
}
|
|
204
|
-
try { require('./llm').setHeadroomRoutes(routes); } catch {}
|
|
205
|
-
state.enabled = true;
|
|
206
|
-
return { ok: true, routes };
|
|
207
|
-
} finally {
|
|
208
|
-
state.starting = false;
|
|
209
|
-
_emit();
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async function setEnabled(on) {
|
|
214
|
-
if (on) {
|
|
215
|
-
state.enabled = true;
|
|
216
|
-
if (!headroomExe()) {
|
|
217
|
-
autoInstall();
|
|
218
|
-
return { ok: false, installing: true, error: 'headroom arka planda kuruluyor — birkaç dakika sonra hazır' };
|
|
219
|
-
}
|
|
220
|
-
return start();
|
|
221
|
-
}
|
|
222
|
-
state.enabled = false;
|
|
223
|
-
stopAll();
|
|
224
|
-
try { require('./llm').setHeadroomRoutes(null); } catch {}
|
|
225
|
-
return { ok: true };
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/* provider listesi değişti (eklendi/silindi) → proxy setini senkronla */
|
|
229
|
-
function syncProviders() {
|
|
230
|
-
if (!state.enabled || !headroomExe()) return;
|
|
231
|
-
start().catch(() => {});
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function stopAll() {
|
|
235
|
-
for (const rec of state.proxies.values()) {
|
|
236
|
-
try { rec.proc.kill(); } catch {}
|
|
237
|
-
try { spawn('taskkill', ['/pid', String(rec.proc.pid), '/T', '/F'], { windowsHide: true }); } catch {}
|
|
238
|
-
}
|
|
239
|
-
state.proxies.clear();
|
|
240
|
-
_emit();
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function setProviderGetter(fn) { state.providerGetter = typeof fn === 'function' ? fn : null; }
|
|
244
|
-
function setStatusSink(fn) { state.statusSink = typeof fn === 'function' ? fn : null; }
|
|
245
|
-
|
|
246
|
-
/* proxy /stats özeti (sağlık duvarı yok — hata.yakalanır) */
|
|
247
|
-
async function stats() {
|
|
248
|
-
const rec = [...state.proxies.values()].find((p) => p.ready);
|
|
249
|
-
if (!rec) return { ok: false, running: false };
|
|
250
|
-
try {
|
|
251
|
-
const r = await fetch('http://127.0.0.1:' + rec.port + '/stats', { signal: AbortSignal.timeout(4000) });
|
|
252
|
-
const j = await r.json();
|
|
253
|
-
const s = (j && (j.tokens || j.summary)) || {};
|
|
254
|
-
return {
|
|
255
|
-
ok: true,
|
|
256
|
-
running: true,
|
|
257
|
-
port: rec.port,
|
|
258
|
-
input: Number(s.input) || 0,
|
|
259
|
-
output: Number(s.output) || 0,
|
|
260
|
-
saved: Number(s.saved) || 0,
|
|
261
|
-
proxyCompressionSaved: Number(s.proxy_compression_saved) || 0,
|
|
262
|
-
savingsPercent: Number(s.savings_percent) || 0,
|
|
263
|
-
allLayersSaved: Number(s.all_layers_saved) || 0,
|
|
264
|
-
};
|
|
265
|
-
} catch {
|
|
266
|
-
return { ok: false, running: true, port: rec.port };
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
module.exports = {
|
|
271
|
-
probe,
|
|
272
|
-
autoInstall,
|
|
273
|
-
setEnabled,
|
|
274
|
-
syncProviders,
|
|
275
|
-
stopAll,
|
|
276
|
-
setProviderGetter,
|
|
277
|
-
setStatusSink,
|
|
278
|
-
stats,
|
|
279
|
-
};
|