beast-agent 2.12.0 → 2.14.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/android.js +265 -0
- package/src/agent/engine.js +79 -0
- package/src/agent/headroom.js +279 -0
- package/src/agent/llm.js +20 -2
- package/src/main.js +209 -0
- package/src/preload.js +8 -0
- package/src/renderer/i18n.js +7 -3
- package/src/renderer/index.html +20 -0
- package/src/renderer/renderer.js +289 -83
- package/src/renderer/style.css +118 -0
package/package.json
CHANGED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* ---------- ANDROID EMÜLATÖR (İSTEĞE BAĞLI — Beast Code mobil önizleme) ----------
|
|
4
|
+
Ayarlardan (Kurulum) görünür; KUR denince arka planda HAFİF set kurar:
|
|
5
|
+
JDK 17 (winget, java yoksa) + Android cmdline-tools (~150MB) +
|
|
6
|
+
platform-tools/emulator + 1 sistem imajı + 1 AVD ("Beast").
|
|
7
|
+
SDK'yı standarda yerleştirir: %LOCALAPPDATA%\Android\Sdk (Android Studio ile
|
|
8
|
+
paylaşımlı konum — kullanıcıda zaten varsa KURULUM YAPMAZ, onu kullanır).
|
|
9
|
+
Beast Code preview akışı: AVD varsa ajan `npx expo start --android` ile
|
|
10
|
+
uygulamayı emülatörde açar (expo AVD'yi adb üzerinden kendisi bulur). */
|
|
11
|
+
|
|
12
|
+
const { spawn, execSync } = require('child_process');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const { emitInstallProgress } = require('./progressbus');
|
|
17
|
+
|
|
18
|
+
const SDK_DIR = path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'Android', 'Sdk');
|
|
19
|
+
const CMDTOOLS_URL = 'https://dl.google.com/android/repository/commandlinetools-win-11076708_latest.zip';
|
|
20
|
+
const SYS_IMAGE = 'system-images;android-34;google_apis;x86_64';
|
|
21
|
+
const AVD_NAME = 'Beast';
|
|
22
|
+
|
|
23
|
+
const state = {
|
|
24
|
+
installing: false,
|
|
25
|
+
installFailed: false,
|
|
26
|
+
installError: '',
|
|
27
|
+
stage: '',
|
|
28
|
+
booting: false,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function sdkCandidates() {
|
|
32
|
+
const list = [];
|
|
33
|
+
for (const v of [process.env.ANDROID_HOME, process.env.ANDROID_SDK_ROOT]) {
|
|
34
|
+
if (v && fs.existsSync(v)) list.push(path.resolve(String(v)));
|
|
35
|
+
}
|
|
36
|
+
list.push(SDK_DIR);
|
|
37
|
+
return list.filter((p) => fs.existsSync(p));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function findSdk(rel, name) {
|
|
41
|
+
for (const sdk of sdkCandidates()) {
|
|
42
|
+
const p = path.join(sdk, ...rel.split('/'), name);
|
|
43
|
+
if (fs.existsSync(p)) return p;
|
|
44
|
+
}
|
|
45
|
+
/* PATH'te ara (kullanıcı elle kurduysa) */
|
|
46
|
+
try {
|
|
47
|
+
const out = String(execSync(`where ${name}`, { timeout: 5000, stdio: 'pipe', encoding: 'utf8', windowsHide: true }));
|
|
48
|
+
const first = out.split(/\r?\n/).find(Boolean);
|
|
49
|
+
if (first) return String(first).trim();
|
|
50
|
+
} catch {}
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findAdb() { return findSdk('platform-tools', 'adb.exe'); }
|
|
55
|
+
|
|
56
|
+
function findEmulator() { return findSdk('emulator', 'emulator.exe'); }
|
|
57
|
+
|
|
58
|
+
function javaOk() {
|
|
59
|
+
try {
|
|
60
|
+
const out = String(execSync('java -version 2>&1', { timeout: 6000, stdio: 'pipe', encoding: 'utf8', windowsHide: true }));
|
|
61
|
+
const m = /version "?(\d+)/.exec(out);
|
|
62
|
+
return m ? Number(m[1]) >= 17 : false;
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function listAvds() {
|
|
69
|
+
const emu = findEmulator();
|
|
70
|
+
if (!emu) return [];
|
|
71
|
+
try {
|
|
72
|
+
const out = String(execSync(`"${emu}" -list-avds`, { timeout: 10000, stdio: 'pipe', encoding: 'utf8', windowsHide: true }));
|
|
73
|
+
return out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
74
|
+
} catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function probe() {
|
|
80
|
+
const adb = findAdb();
|
|
81
|
+
const emu = findEmulator();
|
|
82
|
+
return {
|
|
83
|
+
sdk: !!(adb && emu),
|
|
84
|
+
sdkPath: sdkCandidates()[0] || '',
|
|
85
|
+
adb,
|
|
86
|
+
emulator: emu,
|
|
87
|
+
installing: state.installing,
|
|
88
|
+
failed: state.installFailed,
|
|
89
|
+
error: state.installError,
|
|
90
|
+
booting: state.booting,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* ---------- ARKA PLAN KURULUM ----------
|
|
95
|
+
Aşamalar (progress bus 'android'): 1 java → 2 cmdline-tools → 3 sdk paketleri
|
|
96
|
+
(en ağır: ~1.4GB) → 4 AVD. Yarıda kalırsa idempotent: yeniden KUR kaldığından değil
|
|
97
|
+
baştan ama varolan parçaları atlar (licenses/paket kontrolü). */
|
|
98
|
+
function autoInstall() {
|
|
99
|
+
if (state.installing) return { ok: true, installing: true };
|
|
100
|
+
if (probe().adb && probe().emu) return { ok: true, already: true };
|
|
101
|
+
state.installing = true;
|
|
102
|
+
state.installFailed = false;
|
|
103
|
+
state.installError = '';
|
|
104
|
+
runInstall().catch((e) => {
|
|
105
|
+
state.installing = false;
|
|
106
|
+
state.installFailed = true;
|
|
107
|
+
state.installError = String((e && e.message) || e).slice(0, 160);
|
|
108
|
+
});
|
|
109
|
+
return { ok: true, installing: true };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* .bat çağırıcı: Node güvenlik yaması (CVE-2024-27980) .bat/.cmd'yi shell'siz
|
|
113
|
+
spawn etmeyi EINVAL'le reddeder — cmd.exe /c üzerinden güvenli çağrı */
|
|
114
|
+
function spawnBat(bat, args, opts = {}) {
|
|
115
|
+
const cmdline = ['"' + bat + '"']
|
|
116
|
+
.concat(args.map((a) => (/[\s;&|^<>(),!%]/.test(a) ? '"' + a + '"' : a)))
|
|
117
|
+
.join(' ');
|
|
118
|
+
return spawn('cmd.exe', ['/d', '/s', '/c', cmdline], { windowsHide: true, ...opts.spawnOpts });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* .bat çağırıcı (GÜVENLİ PATİKA): Node .bat'i shell'siz spawn etmeyi EINVAL'le
|
|
122
|
+
reddeder; cmd.exe'ye args dizisiyle geçmek de tırnak/redirect kaçışında kırılır.
|
|
123
|
+
ÇÖZÜM: geçici wrapper .bat YAZ (redirect + tırnaklar cmd'nin içine kalır),
|
|
124
|
+
wrapper'ı tek tırnaklı /c ile çalıştır. */
|
|
125
|
+
let _batSeq = 0;
|
|
126
|
+
function runBat(bat, args, opts = {}) {
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
const inner = ['"' + bat + '"']
|
|
129
|
+
.concat(args.map((a) => (/[\s;&|^<>(),!%]/.test(a) ? '"' + a + '"' : a)))
|
|
130
|
+
.join(' ') + (opts.redirect ? ' < "' + opts.redirect + '"' : '');
|
|
131
|
+
const wrapper = path.join(os.tmpdir(), 'beast-sdk-' + Date.now() + '-' + (++_batSeq) + '.bat');
|
|
132
|
+
fs.writeFileSync(wrapper, '@echo off\r\n' + inner + '\r\nexit /b %ERRORLEVEL%\r\n', 'utf8');
|
|
133
|
+
/* wrapper'a tüm tırnak/redirect işi bırakılır — .bat spawn için Node shell:true ister */
|
|
134
|
+
const proc = spawn(wrapper, [], { windowsHide: true, shell: true, ...opts.spawnOpts });
|
|
135
|
+
let out = '';
|
|
136
|
+
proc.stdout.on('data', (c) => { out += String(c); opts.onData && opts.onData(String(c)); });
|
|
137
|
+
proc.stderr.on('data', (c) => { out += String(c); });
|
|
138
|
+
proc.on('close', (code) => {
|
|
139
|
+
try { fs.unlinkSync(wrapper); } catch {}
|
|
140
|
+
(code === 0 ? resolve : reject)(code === 0 ? out : new Error(opts.tag + ' exit ' + code + (out ? ' :: ' + out.slice(-300) : '')));
|
|
141
|
+
});
|
|
142
|
+
proc.on('error', (e) => {
|
|
143
|
+
try { fs.unlinkSync(wrapper); } catch {}
|
|
144
|
+
reject(e);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function runInstall() {
|
|
150
|
+
const stage = (pct) => { try { emitInstallProgress('android', { pct }); } catch {} };
|
|
151
|
+
|
|
152
|
+
/* 1) JDK 17 (sdkmanager şartı) — java yoksa winget ile */
|
|
153
|
+
stage(3);
|
|
154
|
+
if (!javaOk()) {
|
|
155
|
+
state.stage = 'JDK 17 kuruluyor (winget)';
|
|
156
|
+
await new Promise((resolve, reject) => {
|
|
157
|
+
const proc = spawn('winget', ['install', '--id', 'Microsoft.OpenJDK.17', '--accept-source-agreements', '--accept-package-agreements', '--silent'], { windowsHide: true, shell: true });
|
|
158
|
+
proc.on('close', (code) => (code === 0 || javaOk() ? resolve() : reject(new Error('JDK kurulumu başarısız (winget)'))));
|
|
159
|
+
proc.on('error', () => reject(new Error('winget bulunamadı — JDK 17 elle kur')));
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
stage(15);
|
|
163
|
+
|
|
164
|
+
/* 2) cmdline-tools indir + aç */
|
|
165
|
+
const toolsBin = path.join(SDK_DIR, 'cmdline-tools', 'latest', 'bin', 'sdkmanager.bat');
|
|
166
|
+
if (!fs.existsSync(toolsBin)) {
|
|
167
|
+
state.stage = 'cmdline-tools indiriliyor (~150MB)';
|
|
168
|
+
const zipPath = path.join(os.tmpdir(), 'commandlinetools.zip');
|
|
169
|
+
const res = await fetch(CMDTOOLS_URL);
|
|
170
|
+
if (!res.ok) throw new Error('cmdline-tools indirilemedi: HTTP ' + res.status);
|
|
171
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
172
|
+
fs.mkdirSync(SDK_DIR, { recursive: true });
|
|
173
|
+
fs.writeFileSync(zipPath, buf);
|
|
174
|
+
stage(35);
|
|
175
|
+
execSync(
|
|
176
|
+
`powershell -NoProfile -Command "Expand-Archive -LiteralPath '${zipPath}' -DestinationPath '${SDK_DIR}' -Force"`,
|
|
177
|
+
{ timeout: 300000, windowsHide: true, stdio: 'pipe' }
|
|
178
|
+
);
|
|
179
|
+
/* zip içi: cmdline-tools/ → cmdline-tools/latest/ olmalı */
|
|
180
|
+
const inner = path.join(SDK_DIR, 'cmdline-tools');
|
|
181
|
+
if (fs.existsSync(inner) && !fs.existsSync(toolsBin)) {
|
|
182
|
+
fs.renameSync(inner, inner + '_tmp');
|
|
183
|
+
fs.mkdirSync(path.join(SDK_DIR, 'cmdline-tools'), { recursive: true });
|
|
184
|
+
fs.renameSync(inner + '_tmp', path.join(SDK_DIR, 'cmdline-tools', 'latest'));
|
|
185
|
+
}
|
|
186
|
+
try { fs.unlinkSync(zipPath); } catch {}
|
|
187
|
+
}
|
|
188
|
+
stage(45);
|
|
189
|
+
|
|
190
|
+
/* 3) lisanslar + paketler (platform-tools + emulator + sistem imajı ~1.3GB)
|
|
191
|
+
Cevaplar stdin spam'iyle DEĞİL dosya redirect'iyle verilir — sdkmanager
|
|
192
|
+
interaktif stdin'i cmd borularında düzgün okumuyor */
|
|
193
|
+
state.stage = 'SDK paketleri iniyor (platform-tools · emulator · sistem imajı)';
|
|
194
|
+
const sysEnv = { ...process.env };
|
|
195
|
+
const yesFile = path.join(os.tmpdir(), 'beast-sdk-yes.txt');
|
|
196
|
+
fs.writeFileSync(yesFile, 'y\n'.repeat(80), 'utf8');
|
|
197
|
+
await runBat(toolsBin, ['--licenses'], {
|
|
198
|
+
tag: 'lisans adımı',
|
|
199
|
+
spawnOpts: { env: sysEnv },
|
|
200
|
+
redirect: yesFile,
|
|
201
|
+
});
|
|
202
|
+
stage(50);
|
|
203
|
+
{
|
|
204
|
+
const pkgs = ['platform-tools', 'emulator', SYS_IMAGE];
|
|
205
|
+
/* GERÇEK PROGRESS: sdkmanager çıktısı "[==== ] 17% Downloading x" biçiminde
|
|
206
|
+
GLOBAL yüzde basar (monotonik artar) — %50-95 aralığına doğrudan eşle */
|
|
207
|
+
let last = 50;
|
|
208
|
+
const lastPercent = /(\d{1,3})%[^%]*$/;
|
|
209
|
+
/* sdkmanager ağ ortasında patlarsa: yarım .temp'i temizle + 3 kere yeniden
|
|
210
|
+
dene (indirilenler korunur — kaldığı yeri hızla atlar) */
|
|
211
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
212
|
+
try {
|
|
213
|
+
try { fs.rmSync(path.join(SDK_DIR, '.temp'), { recursive: true, force: true }); } catch {}
|
|
214
|
+
await runBat(toolsBin, pkgs, {
|
|
215
|
+
tag: 'sdkmanager',
|
|
216
|
+
spawnOpts: { env: sysEnv },
|
|
217
|
+
redirect: yesFile,
|
|
218
|
+
onData: (s) => {
|
|
219
|
+
const m = lastPercent.exec(s);
|
|
220
|
+
if (m) {
|
|
221
|
+
const sdkPct = Math.max(0, Math.min(100, Number(m[1])));
|
|
222
|
+
const pct = 50 + sdkPct * 0.45; /* %0→50, %100→95 */
|
|
223
|
+
if (pct > last + 0.4) { last = pct; stage(pct); }
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
break;
|
|
228
|
+
} catch (e) {
|
|
229
|
+
if (attempt < 3 && !findAdb()) continue;
|
|
230
|
+
throw e;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* 4) AVD yarat */
|
|
236
|
+
stage(96);
|
|
237
|
+
state.stage = 'AVD (emülatör cihazı) oluşturuluyor';
|
|
238
|
+
if (!(await listAvds()).includes(AVD_NAME)) {
|
|
239
|
+
const avdm = path.join(SDK_DIR, 'cmdline-tools', 'latest', 'bin', 'avdmanager.bat');
|
|
240
|
+
const noFile = path.join(os.tmpdir(), 'beast-sdk-no.txt');
|
|
241
|
+
fs.writeFileSync(noFile, 'no\n', 'utf8');
|
|
242
|
+
await runBat(avdm, ['create', 'avd', '-n', AVD_NAME, '-k', SYS_IMAGE, '-d', 'pixel'], {
|
|
243
|
+
tag: 'avdmanager',
|
|
244
|
+
spawnOpts: { env: sysEnv },
|
|
245
|
+
redirect: noFile,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
emitInstallProgress('android', { pct: 100 });
|
|
249
|
+
state.installing = false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* ---------- EMÜLATÖR BAŞLATMA (uzun ömürlü — kendi penceresi açılır) ---------- */
|
|
253
|
+
async function startAvd(name) {
|
|
254
|
+
const emu = findEmulator();
|
|
255
|
+
if (!emu) return { ok: false, error: 'emulator.exe yok — önce Kurulum sekmesinden kur' };
|
|
256
|
+
const target = name || (await listAvds())[0];
|
|
257
|
+
if (!target) return { ok: false, error: 'AVD yok — önce kur' };
|
|
258
|
+
state.booting = true;
|
|
259
|
+
const proc = spawn(emu, ['-avd', target, '-no-snapshot'], { windowsHide: false, detached: false });
|
|
260
|
+
proc.on('exit', () => { state.booting = false; });
|
|
261
|
+
/* boot tamamlanmasını bekleme — arka planda; expo/adb kendisi bekler */
|
|
262
|
+
return { ok: true, avd: target, note: 'emülatör başlatıldı — ilk açılış 30-90 sn sürer' };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = { probe, autoInstall, listAvds, startAvd };
|
package/src/agent/engine.js
CHANGED
|
@@ -1064,6 +1064,39 @@ class Engine {
|
|
|
1064
1064
|
return removed;
|
|
1065
1065
|
}
|
|
1066
1066
|
|
|
1067
|
+
/* Düşünme geri-besleme kurtarması: asistan mesajlarındaki reasoning_content'i
|
|
1068
|
+
bellekten + jsonl'den temizler. "must be passed back" hatasını veren sağlayıcı
|
|
1069
|
+
için tek kurtarma yolu — döndürür: temizlenen mesaj sayısı */
|
|
1070
|
+
_stripAssistantReasoning(session) {
|
|
1071
|
+
let dropped = 0;
|
|
1072
|
+
for (const m of session.messages) {
|
|
1073
|
+
if (m && m.role === 'assistant' && (m.reasoning_content || m.reasoning)) {
|
|
1074
|
+
delete m.reasoning_content;
|
|
1075
|
+
delete m.reasoning;
|
|
1076
|
+
dropped++;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
if (!dropped) return 0;
|
|
1080
|
+
try {
|
|
1081
|
+
const file = this._file(session.id);
|
|
1082
|
+
const lines = fs.readFileSync(file, 'utf8').split('\n');
|
|
1083
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1084
|
+
if (!lines[i].trim()) continue;
|
|
1085
|
+
let r;
|
|
1086
|
+
try { r = JSON.parse(lines[i]); } catch { continue; }
|
|
1087
|
+
if (r.t === 'msg' && r.role === 'assistant' && (r.reasoning_content || r.reasoning)) {
|
|
1088
|
+
delete r.reasoning_content;
|
|
1089
|
+
delete r.reasoning;
|
|
1090
|
+
lines[i] = JSON.stringify(r);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
const tmp = file + '.tmp';
|
|
1094
|
+
fs.writeFileSync(tmp, lines.join('\n'));
|
|
1095
|
+
fs.renameSync(tmp, file);
|
|
1096
|
+
} catch {}
|
|
1097
|
+
return dropped;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1067
1100
|
_load(id) {
|
|
1068
1101
|
if (this.cache.has(id)) return this.cache.get(id);
|
|
1069
1102
|
const file = this._file(id);
|
|
@@ -3240,6 +3273,37 @@ class Engine {
|
|
|
3240
3273
|
arındırıp turu BİR KEZ görüntüsüz dener. */
|
|
3241
3274
|
const msgStr = String((e && e.message) || '');
|
|
3242
3275
|
const aborted = signal && signal.aborted;
|
|
3276
|
+
/* DÜŞÜNME GERİ-BESLEME KURTARMASI: "reasoning_content ... must be passed
|
|
3277
|
+
back" hatası eski oturumlarda (reasoning saklanmadan önce) olur —
|
|
3278
|
+
geçmişi reasoning'siz arındırıp turu BİR KEZ yeniden dene */
|
|
3279
|
+
if (!aborted && /reasoning_content/i.test(msgStr)) {
|
|
3280
|
+
const dropped = this._stripAssistantReasoning(session);
|
|
3281
|
+
if (dropped) {
|
|
3282
|
+
emitSafe(this, session.id, {
|
|
3283
|
+
type: 'status',
|
|
3284
|
+
status: `🧠 düşük uyumlu sağlayıcı: geçmişteki düşünme içerikleri temizlendi (${dropped} mesaj) — tur yeniden deneniyor`,
|
|
3285
|
+
});
|
|
3286
|
+
payload = this._buildPayload(system, session.messages, session.notes, dynBudget, session.summary);
|
|
3287
|
+
res = await this._streamWithFallbacks(
|
|
3288
|
+
session,
|
|
3289
|
+
payload,
|
|
3290
|
+
activeTools,
|
|
3291
|
+
signal,
|
|
3292
|
+
onDelta,
|
|
3293
|
+
sel,
|
|
3294
|
+
role === 'vision'
|
|
3295
|
+
);
|
|
3296
|
+
const actualR = res.usage && res.usage.prompt_tokens;
|
|
3297
|
+
if (actualR > 0) {
|
|
3298
|
+
const predicted = this._payloadTokens(payload);
|
|
3299
|
+
if (predicted > 50) {
|
|
3300
|
+
const r = clamp(actualR / predicted, 0.75, 3);
|
|
3301
|
+
this.tokRatio = clamp(this.tokRatio * 0.7 + r * 0.3, 0.75, 3);
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
return res;
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3243
3307
|
const imgish =
|
|
3244
3308
|
!aborted &&
|
|
3245
3309
|
/image|vision|multimodal|no endpoints/i.test(msgStr);
|
|
@@ -3395,6 +3459,10 @@ class Engine {
|
|
|
3395
3459
|
|
|
3396
3460
|
const assistant = { role: 'assistant', content: res.content || '' };
|
|
3397
3461
|
if (res.toolCalls && res.toolCalls.length) assistant.tool_calls = res.toolCalls;
|
|
3462
|
+
/* DÜŞÜNME MODU GERİ-BESLEMESİ: Console Go gibi sağlayıcılar thinking
|
|
3463
|
+
modunda reasoning_content'in GEÇMİŞTE geri gönderilmesini şart koşar
|
|
3464
|
+
("must be passed back to the API") — saklamazsak sonraki tur 400'le ölür */
|
|
3465
|
+
if (res.reasoning) assistant.reasoning_content = res.reasoning;
|
|
3398
3466
|
session.messages.push(assistant);
|
|
3399
3467
|
try {
|
|
3400
3468
|
this._append(session, assistant);
|
|
@@ -3529,6 +3597,16 @@ class Engine {
|
|
|
3529
3597
|
else if (/\b(npm (run )?(dev|start)|npx serve|live-server)\b/i.test(cmdStr)) port = 3000;
|
|
3530
3598
|
if (port) bcArtifacts.serverUrl = 'http://localhost:' + port;
|
|
3531
3599
|
}
|
|
3600
|
+
/* Expo dev server: exp://<LAN-IP>:<port> çıktıdan yakalanır —
|
|
3601
|
+
telefon/Expo Go için QR renderer'a canlı iletilir */
|
|
3602
|
+
const xm = /exp:\/\/[^\s'"<>]+/i.exec(hay);
|
|
3603
|
+
if (xm) {
|
|
3604
|
+
const eurl = xm[0].replace(/[)\].,;:'"]+$/, '');
|
|
3605
|
+
if (bcArtifacts.expUrl !== eurl) {
|
|
3606
|
+
bcArtifacts.expUrl = eurl;
|
|
3607
|
+
emit({ type: 'bc-expurl', url: eurl });
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3532
3610
|
}
|
|
3533
3611
|
} catch {}
|
|
3534
3612
|
}
|
|
@@ -4068,6 +4146,7 @@ const skills = require('./skills');
|
|
|
4068
4146
|
);
|
|
4069
4147
|
const assistant = { role: 'assistant', content: res.content || '' };
|
|
4070
4148
|
if (res.toolCalls && res.toolCalls.length) assistant.tool_calls = res.toolCalls;
|
|
4149
|
+
if (res.reasoning) assistant.reasoning_content = res.reasoning; /* thinking geri-besleme */
|
|
4071
4150
|
msgs.push(assistant);
|
|
4072
4151
|
if (!res.toolCalls || !res.toolCalls.length) return res.content || '(alt-agent boş döndü)';
|
|
4073
4152
|
for (const tc of res.toolCalls) {
|
|
@@ -0,0 +1,279 @@
|
|
|
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
|
+
};
|