beast-agent 2.19.0 → 2.20.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/customtools.js +242 -0
- package/src/agent/engine.js +34 -5
- package/src/main.js +58 -7
- package/src/preload.js +5 -0
- package/src/renderer/index.html +42 -2
- package/src/renderer/renderer.js +281 -1
- package/src/renderer/style.css +233 -0
package/package.json
CHANGED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* KİŞİSEL TOOLLAR: %APPDATA%\beast\tools\<slug>\tool.json + run.js
|
|
4
|
+
Kullanıcı (ya da ajan) Node ile koşan kendi araçlarını yazar; TÜM oturumlar
|
|
5
|
+
— BEAST FINANCE dahil — bunları model'e açılan gerçek araç olarak çağırır
|
|
6
|
+
(tool__<slug>). Sözleşme:
|
|
7
|
+
- stdin : JSON argümanlar
|
|
8
|
+
- stdout : JSON sonuç (son satır JSON'a çevrilmeye çalışılır)
|
|
9
|
+
- çalışma klasörü: tool'un kendi klasörü (%APPDATA%\beast\tools\<slug>)
|
|
10
|
+
Sonuç model'e { ok, ... } olarak döner; çocuk proses izoledir (uygulama
|
|
11
|
+
çökmez), 90 sn zaman aşımı vardır. */
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { spawn } = require('child_process');
|
|
16
|
+
const { beastRoot } = require('./memory');
|
|
17
|
+
|
|
18
|
+
let notify = () => {};
|
|
19
|
+
function setNotify(fn) {
|
|
20
|
+
notify = typeof fn === 'function' ? fn : () => {};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function dir() {
|
|
24
|
+
return path.join(beastRoot(), 'tools');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function slugify(name) {
|
|
28
|
+
return String(name || '')
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.replace(/[^a-z0-9_-]+/g, '_')
|
|
31
|
+
.replace(/^_+|_+$/g, '')
|
|
32
|
+
.slice(0, 24);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let cache = null;
|
|
36
|
+
let dirty = true;
|
|
37
|
+
|
|
38
|
+
function invalidate() {
|
|
39
|
+
dirty = true;
|
|
40
|
+
cache = null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readTool(d) {
|
|
44
|
+
try {
|
|
45
|
+
const json = JSON.parse(fs.readFileSync(path.join(d, 'tool.json'), 'utf8'));
|
|
46
|
+
if (!json || typeof json !== 'object' || !json.name) return null;
|
|
47
|
+
const runJs = path.join(d, 'run.js');
|
|
48
|
+
if (!fs.existsSync(runJs)) return null;
|
|
49
|
+
return {
|
|
50
|
+
id: path.basename(d),
|
|
51
|
+
name: String(json.name).slice(0, 40),
|
|
52
|
+
description: String(json.description || '').slice(0, 300),
|
|
53
|
+
parameters:
|
|
54
|
+
json.parameters && typeof json.parameters === 'object'
|
|
55
|
+
? json.parameters
|
|
56
|
+
: { type: 'object', properties: {} },
|
|
57
|
+
script: runJs,
|
|
58
|
+
code: fs.readFileSync(runJs, 'utf8').slice(0, 20000),
|
|
59
|
+
updatedAt: json.updatedAt || null,
|
|
60
|
+
};
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function scan() {
|
|
67
|
+
const out = [];
|
|
68
|
+
try {
|
|
69
|
+
for (const e of fs.readdirSync(dir(), { withFileTypes: true })) {
|
|
70
|
+
if (!e.isDirectory()) continue;
|
|
71
|
+
const t = readTool(path.join(dir(), e.name));
|
|
72
|
+
if (t) out.push(t);
|
|
73
|
+
}
|
|
74
|
+
} catch {}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function list() {
|
|
79
|
+
return scan();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* engine _chatTurn'e OpenAI function formatında şemalar */
|
|
83
|
+
function definitions() {
|
|
84
|
+
if (dirty || !cache) {
|
|
85
|
+
cache = scan();
|
|
86
|
+
dirty = false;
|
|
87
|
+
}
|
|
88
|
+
return cache.map((t) => ({
|
|
89
|
+
type: 'function',
|
|
90
|
+
function: {
|
|
91
|
+
name: 'tool__' + t.id,
|
|
92
|
+
description:
|
|
93
|
+
(t.description || 'kişisel tool') + ` (kişisel tool — args stdin JSON, sonuç stdout JSON)`,
|
|
94
|
+
parameters: t.parameters,
|
|
95
|
+
},
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function names() {
|
|
100
|
+
if (dirty || !cache) {
|
|
101
|
+
cache = scan();
|
|
102
|
+
dirty = false;
|
|
103
|
+
}
|
|
104
|
+
return cache.map((t) => 'tool__' + t.id);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const TOOL_TIMEOUT_MS = 90 * 1000;
|
|
108
|
+
|
|
109
|
+
/* Tool'u çocuk node prosesinde koşturur — uygulama asla çökmez */
|
|
110
|
+
function call(name, args) {
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
const id = String(name || '').replace(/^tool__/, '');
|
|
113
|
+
if (!id) return resolve({ ok: false, error: 'tool adı gerekli' });
|
|
114
|
+
const t = scan().find((x) => x.id === id);
|
|
115
|
+
if (!t) return resolve({ ok: false, error: 'tool bulunamadı: ' + id });
|
|
116
|
+
const t0 = Date.now();
|
|
117
|
+
let child = null;
|
|
118
|
+
let out = '';
|
|
119
|
+
let err = '';
|
|
120
|
+
let done = false;
|
|
121
|
+
const finish = (r) => {
|
|
122
|
+
if (done) return;
|
|
123
|
+
done = true;
|
|
124
|
+
clearTimeout(killer);
|
|
125
|
+
try {
|
|
126
|
+
notify({
|
|
127
|
+
at: new Date().toISOString(),
|
|
128
|
+
tool: t.name,
|
|
129
|
+
toolId: id,
|
|
130
|
+
ok: !!r.ok,
|
|
131
|
+
ms: Date.now() - t0,
|
|
132
|
+
error: r && r.error ? String(r.error).slice(0, 200) : null,
|
|
133
|
+
});
|
|
134
|
+
} catch {}
|
|
135
|
+
resolve(r);
|
|
136
|
+
};
|
|
137
|
+
let killer = setTimeout(() => {
|
|
138
|
+
try { child && child.kill(); } catch {}
|
|
139
|
+
finish({ ok: false, error: 'tool zaman aşımı (' + Math.round(TOOL_TIMEOUT_MS / 1000) + ' sn)' });
|
|
140
|
+
}, TOOL_TIMEOUT_MS);
|
|
141
|
+
try {
|
|
142
|
+
child = spawn(process.execPath, [t.script], { cwd: t.path, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
143
|
+
} catch (e) {
|
|
144
|
+
return finish({ ok: false, error: String((e && e.message) || e) });
|
|
145
|
+
}
|
|
146
|
+
child.stdout.on('data', (d) => { out += d; });
|
|
147
|
+
child.stderr.on('data', (d) => { err += d; });
|
|
148
|
+
child.on('error', (e) => finish({ ok: false, error: String((e && e.message) || e) }));
|
|
149
|
+
child.on('close', (code) => {
|
|
150
|
+
const raw = String(out || '').trim();
|
|
151
|
+
let data = null;
|
|
152
|
+
try { data = raw ? JSON.parse(raw) : null; } catch {}
|
|
153
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data) || typeof data.ok !== 'boolean') {
|
|
154
|
+
data = { ok: code === 0, result: data === null ? raw.slice(0, 4000) : data };
|
|
155
|
+
}
|
|
156
|
+
if (code !== 0) {
|
|
157
|
+
if (data.ok !== false) data.ok = false;
|
|
158
|
+
if (!data.error) data.error = String(err || '').slice(0, 400) || 'çıkış kodu ' + code;
|
|
159
|
+
}
|
|
160
|
+
finish(data);
|
|
161
|
+
});
|
|
162
|
+
try {
|
|
163
|
+
child.stdin.write(JSON.stringify(args || {}));
|
|
164
|
+
child.stdin.end();
|
|
165
|
+
} catch {}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function save({ id, name, description, parameters, code }) {
|
|
170
|
+
const slug = id && /^[a-z0-9_-]{2,24}$/.test(String(id)) ? String(id) : slugify(name);
|
|
171
|
+
if (!slug) return { ok: false, error: 'tool adı gerekli (a-z 0-9 _ -)' };
|
|
172
|
+
const d = path.join(dir(), slug);
|
|
173
|
+
try {
|
|
174
|
+
fs.mkdirSync(d, { recursive: true });
|
|
175
|
+
fs.writeFileSync(
|
|
176
|
+
path.join(d, 'tool.json'),
|
|
177
|
+
JSON.stringify(
|
|
178
|
+
{
|
|
179
|
+
name: String(name || slug).slice(0, 40),
|
|
180
|
+
description: String(description || '').slice(0, 300),
|
|
181
|
+
parameters:
|
|
182
|
+
parameters && typeof parameters === 'object' && !Array.isArray(parameters)
|
|
183
|
+
? parameters
|
|
184
|
+
: { type: 'object', properties: {} },
|
|
185
|
+
updatedAt: new Date().toISOString(),
|
|
186
|
+
},
|
|
187
|
+
null,
|
|
188
|
+
2
|
|
189
|
+
)
|
|
190
|
+
);
|
|
191
|
+
const js = String(code || '').trim();
|
|
192
|
+
if (!js) return { ok: false, error: 'run.js kodu boş olamaz' };
|
|
193
|
+
fs.writeFileSync(path.join(d, 'run.js'), js.slice(0, 20000));
|
|
194
|
+
invalidate();
|
|
195
|
+
return { ok: true, id: slug };
|
|
196
|
+
} catch (e) {
|
|
197
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function remove(id) {
|
|
202
|
+
const slug = String(id || '');
|
|
203
|
+
if (!/^[a-z0-9_-]{2,24}$/.test(slug)) return { ok: false, error: 'geçersiz tool kimliği' };
|
|
204
|
+
try {
|
|
205
|
+
fs.rmSync(path.join(dir(), slug), { recursive: true, force: true });
|
|
206
|
+
invalidate();
|
|
207
|
+
return { ok: true };
|
|
208
|
+
} catch (e) {
|
|
209
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* Örnek tool: klasör boşsa biçimi göstermek için bir adet kur */
|
|
214
|
+
function seedIfEmpty() {
|
|
215
|
+
try {
|
|
216
|
+
if (scan().length) return;
|
|
217
|
+
save({
|
|
218
|
+
name: 'ornek_echo',
|
|
219
|
+
description:
|
|
220
|
+
'Örnek kişisel tool — gelen argümanları geri döndürür. Bu klasörü kopyalayarak kendi toollarını yaz; agent finance dahil tüm oturumlarda tool__<ad> olarak çağırır.',
|
|
221
|
+
parameters: {
|
|
222
|
+
type: 'object',
|
|
223
|
+
properties: {
|
|
224
|
+
mesaj: { type: 'string', description: 'geri döndürülecek deneme mesajı' },
|
|
225
|
+
},
|
|
226
|
+
required: ['mesaj'],
|
|
227
|
+
},
|
|
228
|
+
code:
|
|
229
|
+
"'use strict';\n" +
|
|
230
|
+
"// KİŞİSEL TOOL — stdin: JSON args · stdout: JSON sonuç\n" +
|
|
231
|
+
"// Bu klasörü kopyalayıp kendi araçlarını yaz (örn. MT5 emir köprüsü, özel API çağrısı…)\n" +
|
|
232
|
+
"const fs = require('fs');\n" +
|
|
233
|
+
"let raw = '';\n" +
|
|
234
|
+
"try { raw = fs.readFileSync(0, 'utf8'); } catch {}\n" +
|
|
235
|
+
"let args = {};\n" +
|
|
236
|
+
"try { args = JSON.parse(raw || '{}'); } catch {}\n" +
|
|
237
|
+
"console.log(JSON.stringify({ ok: true, echo: args, not: 'run.js ve tool.json düzenlenebilir' }));\n",
|
|
238
|
+
});
|
|
239
|
+
} catch {}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = { setNotify, list, scan, definitions, names, call, save, remove, seedIfEmpty, invalidate, dir, slugify };
|
package/src/agent/engine.js
CHANGED
|
@@ -13,6 +13,7 @@ const tools = require('./tools');
|
|
|
13
13
|
let financetools = null;
|
|
14
14
|
try { financetools = require('./financetools'); } catch {}
|
|
15
15
|
const FINANCE_TOOL_SET = new Set(financetools ? financetools.NAMES : []);
|
|
16
|
+
const customtools = require('./customtools');
|
|
16
17
|
const research = require('./research');
|
|
17
18
|
const agentdefs = require('./agentdefs');
|
|
18
19
|
const memory = require('./memory');
|
|
@@ -1232,7 +1233,14 @@ class Engine {
|
|
|
1232
1233
|
let r;
|
|
1233
1234
|
try { r = JSON.parse(l); } catch { continue; }
|
|
1234
1235
|
if (r.t === 'meta') out.push(JSON.stringify({ t: 'meta', id: s.id, code: s.code, createdAt: s.createdAt }));
|
|
1235
|
-
|
|
1236
|
+
/* KALICİ ETİKETLER compact'ta ASLA düşmez — özellikle 'fin'/'finoff':
|
|
1237
|
+
silinirse finance oturumu etiketini KAYBEDER ve mt5_* araçları
|
|
1238
|
+
"unknown tool" olur (bayat compact kaybı bu yüzden) */
|
|
1239
|
+
else if (
|
|
1240
|
+
r.t === 'meta2' || r.t === 'bot' || r.t === 'todo' ||
|
|
1241
|
+
r.t === 'fin' || r.t === 'finoff' ||
|
|
1242
|
+
r.t === 'agent' || r.t === 'bcws' || r.t === 'botdm'
|
|
1243
|
+
) out.push(l);
|
|
1236
1244
|
}
|
|
1237
1245
|
if (lastNotesLine) out.push(lastNotesLine);
|
|
1238
1246
|
/* korunan pencere: son N mesaj */
|
|
@@ -1419,7 +1427,12 @@ class Engine {
|
|
|
1419
1427
|
const kept = lines.filter((l) => {
|
|
1420
1428
|
try {
|
|
1421
1429
|
const r = JSON.parse(l);
|
|
1422
|
-
|
|
1430
|
+
/* kalıcı etiketler korunur (fin/finoff dahil — finance bayrağı kaybolmasın) */
|
|
1431
|
+
return (
|
|
1432
|
+
r.t === 'meta' || r.t === 'meta2' || r.t === 'bot' || r.t === 'todo' ||
|
|
1433
|
+
r.t === 'fin' || r.t === 'finoff' ||
|
|
1434
|
+
r.t === 'agent' || r.t === 'bcws' || r.t === 'botdm'
|
|
1435
|
+
);
|
|
1423
1436
|
} catch { return false; }
|
|
1424
1437
|
});
|
|
1425
1438
|
const tmp = file + '.tmp';
|
|
@@ -3453,6 +3466,9 @@ class Engine {
|
|
|
3453
3466
|
toolsList = await mcp.mergeTools(toolsList);
|
|
3454
3467
|
/* Beast Apps: kurulu app'lerin araçları (app__<id>__<tool>) modele açılır */
|
|
3455
3468
|
toolsList = apps.mergeTools(toolsList);
|
|
3469
|
+
/* KİŞİSEL TOOLLAR: %APPDATA%\beast\tools\<ad>\ — kullanıcı yazmışsa
|
|
3470
|
+
TÜM oturumlara (BEAST FINANCE dahil) tool__<ad> olarak açılır */
|
|
3471
|
+
toolsList = [...toolsList, ...customtools.definitions()];
|
|
3456
3472
|
/* Beast Finance: skill aracı da açık — kataloğun SKILL.md gövdesi
|
|
3457
3473
|
modele açılır (skill handler'ı zaten tüm oturumlar için çalışır,
|
|
3458
3474
|
definition yalnız BC'de vardı; finance chat copilot + trader ikisi de)
|
|
@@ -3464,8 +3480,13 @@ class Engine {
|
|
|
3464
3480
|
if (skillDef) toolsList = [...toolsList, skillDef];
|
|
3465
3481
|
}
|
|
3466
3482
|
/* AJAN DM: koşan ajan oturumlarına (arka plan işleri + finance
|
|
3467
|
-
ajanları)
|
|
3468
|
-
|
|
3483
|
+
ajanları) VE ana sohbete verilir — chat Beast'i de ajana DM atabilir;
|
|
3484
|
+
müşteri bot oturumları (botId ≠ beast) ve botlar arası DM hariç */
|
|
3485
|
+
if (
|
|
3486
|
+
session &&
|
|
3487
|
+
!session.isBotDm &&
|
|
3488
|
+
(session.bgJob || session.finance || !session.botId || session.botId === 'beast')
|
|
3489
|
+
) {
|
|
3469
3490
|
toolsList = [...toolsList, AGENT_DM_DEF];
|
|
3470
3491
|
}
|
|
3471
3492
|
/* panel_run yalnız SANDBOX oturumlarında görünsün — diğer panellerde
|
|
@@ -3890,6 +3911,8 @@ class Engine {
|
|
|
3890
3911
|
// Paralel yürütme — bağımsız çağrılar beklemesin
|
|
3891
3912
|
session.toolsSinceReflect = (session.toolsSinceReflect || 0) + res.toolCalls.length;
|
|
3892
3913
|
if (!this._knownToolNames) this._knownToolNames = new Set([...TOOLS.map((t) => t.function.name), ...opencode.toolmap.names()]);
|
|
3914
|
+
/* kişisel tool sonradan yazılabilir — isim havuzunu canlı tazele */
|
|
3915
|
+
for (const n of customtools.names()) this._knownToolNames.add(n);
|
|
3893
3916
|
await Promise.all(
|
|
3894
3917
|
res.toolCalls.map(async (tc) => {
|
|
3895
3918
|
let name = tc.function && tc.function.name;
|
|
@@ -4765,6 +4788,10 @@ const skills = require('./skills');
|
|
|
4765
4788
|
if (name === 'agent_dm') {
|
|
4766
4789
|
return JSON.stringify(this._agentDmSend(sessionId, args));
|
|
4767
4790
|
}
|
|
4791
|
+
/* KİŞİSEL TOOL: %APPDATA%\beast\tools\ — çocuk node prosesinde izole koşar */
|
|
4792
|
+
if (String(name).startsWith('tool__')) {
|
|
4793
|
+
return JSON.stringify(await customtools.call(name, args));
|
|
4794
|
+
}
|
|
4768
4795
|
if (isBc) {
|
|
4769
4796
|
/* opencode permission akışı (tool/external-directory.ts + her aracın
|
|
4770
4797
|
ctx.ask'ı): önce workspace DIŞI erişim izni, sonra araç izni.
|
|
@@ -4796,7 +4823,9 @@ const skills = require('./skills');
|
|
|
4796
4823
|
"always" onaylı araçlar doğrudan geçer. (BC dışı oturumlar — opencode
|
|
4797
4824
|
izin sistemi yalnız Beast Code oturumlarında koşar) */
|
|
4798
4825
|
if (
|
|
4799
|
-
(Engine.RISKY_TOOLS.has(name) ||
|
|
4826
|
+
(Engine.RISKY_TOOLS.has(name) ||
|
|
4827
|
+
String(name).startsWith('mcp__') ||
|
|
4828
|
+
String(name).startsWith('tool__')) && /* kişisel tool = kullanıcı kodu koşturur */
|
|
4800
4829
|
!this.alwaysAllowTools.has(name) &&
|
|
4801
4830
|
this.approvals && typeof this.approvals.request === 'function'
|
|
4802
4831
|
) {
|
package/src/main.js
CHANGED
|
@@ -3605,6 +3605,7 @@ app.whenReady().then(() => {
|
|
|
3605
3605
|
reloadBackend();
|
|
3606
3606
|
syncWhitelist(); // bot sistemi: whitelist.json aynası ilk açılışta garanti
|
|
3607
3607
|
try { bots.ensureBotCodes(); } catch {} // her bota benzersiz 5 haneli kod garanti
|
|
3608
|
+
try { customtools.seedIfEmpty(); } catch {} // kişisel toollar klasörü boşsa örnek tool kur
|
|
3608
3609
|
createSplash();
|
|
3609
3610
|
createWindow();
|
|
3610
3611
|
log.info('main', 'Beast Agent başlatıldı');
|
|
@@ -5892,6 +5893,27 @@ ipcMain.handle('agents:cancel', (_e, id) => {
|
|
|
5892
5893
|
/* AJAN DM paneli: ajanlar arası mesaj arşivi */
|
|
5893
5894
|
ipcMain.handle('agent-dms:list', () => (engine ? engine.agentDmsList() : []));
|
|
5894
5895
|
ipcMain.handle('agent-dms:clear', () => (engine ? engine.agentDmsClear() : { ok: false }));
|
|
5896
|
+
|
|
5897
|
+
/* ---------- KİŞİSEL TOOLLAR (%APPDATA%\beast\tools\) ---------- */
|
|
5898
|
+
ipcMain.handle('tools:list', () => customtools.list());
|
|
5899
|
+
ipcMain.handle('tools:save', (_e, tool) => customtools.save(tool || {}));
|
|
5900
|
+
ipcMain.handle('tools:delete', (_e, id) => customtools.remove(id));
|
|
5901
|
+
ipcMain.handle('tools:run', async (_e, payload) => {
|
|
5902
|
+
const id = String((payload && payload.id) || '');
|
|
5903
|
+
const args = (payload && payload.args) || {};
|
|
5904
|
+
const r = await customtools.call('tool__' + id, args);
|
|
5905
|
+
return r;
|
|
5906
|
+
});
|
|
5907
|
+
ipcMain.handle('tools:openFolder', () => {
|
|
5908
|
+
try {
|
|
5909
|
+
const d = customtools.dir();
|
|
5910
|
+
fs.mkdirSync(d, { recursive: true });
|
|
5911
|
+
require('electron').shell.openPath(d);
|
|
5912
|
+
return { ok: true };
|
|
5913
|
+
} catch (e) {
|
|
5914
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
5915
|
+
}
|
|
5916
|
+
});
|
|
5895
5917
|
ipcMain.handle('ceo:get', () => !!engine.ceoMode);
|
|
5896
5918
|
ipcMain.handle('ceo:set', (_e, v) => {
|
|
5897
5919
|
settings.ceoMode = !!v;
|
|
@@ -8273,6 +8295,7 @@ function studioWatchStop() {
|
|
|
8273
8295
|
oturumu, tur tur piyasa tarayıp (farklı API/model seçilebilir) işlem kovalar. */
|
|
8274
8296
|
const mt5bridge = require('./mt5bridge');
|
|
8275
8297
|
const financetools = require('./agent/financetools');
|
|
8298
|
+
const customtools = require('./agent/customtools');
|
|
8276
8299
|
|
|
8277
8300
|
const financeState = {
|
|
8278
8301
|
mode: false, /* chat oturumları finance bayrağı alıyor mu */
|
|
@@ -8337,6 +8360,15 @@ try {
|
|
|
8337
8360
|
});
|
|
8338
8361
|
} catch {}
|
|
8339
8362
|
|
|
8363
|
+
/* KİŞİSEL TOOLLAR: çalışma kaydı renderer'a (TOOLS konsolu buradan beslenir) */
|
|
8364
|
+
try {
|
|
8365
|
+
customtools.setNotify((entry) => {
|
|
8366
|
+
try {
|
|
8367
|
+
if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'tool-log', ...entry });
|
|
8368
|
+
} catch {}
|
|
8369
|
+
});
|
|
8370
|
+
} catch {}
|
|
8371
|
+
|
|
8340
8372
|
function financeEnsureBridge() {
|
|
8341
8373
|
const f = finCfg();
|
|
8342
8374
|
const candidates = [String(f.pythonPath || '').trim(), 'python', 'py -3'].filter(Boolean);
|
|
@@ -8695,23 +8727,32 @@ ipcMain.handle('finance:snapshot', async () => {
|
|
|
8695
8727
|
ipcMain.handle('finance:mode', async (_e, payload) => {
|
|
8696
8728
|
financeState.mode = !!(payload && payload.on);
|
|
8697
8729
|
const sid = String((payload && payload.sessionId) || '');
|
|
8730
|
+
let needNew = false;
|
|
8698
8731
|
if (sid && engine) {
|
|
8699
8732
|
try {
|
|
8700
8733
|
let s = engine.cache.get(sid);
|
|
8701
8734
|
if (!s) { try { s = engine._load(sid); } catch {} }
|
|
8702
|
-
if (s && !s.
|
|
8735
|
+
if (s && !s.bgJob && (!s.botId || s.botId === 'beast')) {
|
|
8703
8736
|
if (financeState.mode) {
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8737
|
+
if (s.finance) {
|
|
8738
|
+
/* zaten finance oturumu — aynen sürdür */
|
|
8739
|
+
finApplyTraderFields(s);
|
|
8740
|
+
s.financeTrader = false; /* chat copilot'ı — trader değil */
|
|
8741
|
+
} else {
|
|
8742
|
+
/* NORMAL oturum (örn. WhatsApp sohbeti) finance modundayken ASLA
|
|
8743
|
+
finance'e çevrilmez — ayrı bir finance oturumu açılır.
|
|
8744
|
+
(eski davranış WhatsApp oturumunu da finance listesine karıştırıyordu) */
|
|
8745
|
+
needNew = true;
|
|
8746
|
+
}
|
|
8707
8747
|
} else {
|
|
8708
|
-
|
|
8748
|
+
/* mod kapandı: aktif finance oturumu normal sohbete döner */
|
|
8749
|
+
if (s.finance) engine.unmarkFinance(sid);
|
|
8709
8750
|
}
|
|
8710
8751
|
engine.cache.set(sid, s);
|
|
8711
8752
|
}
|
|
8712
8753
|
} catch {}
|
|
8713
8754
|
}
|
|
8714
|
-
return { ok: true, mode: financeState.mode };
|
|
8755
|
+
return { ok: true, mode: financeState.mode, needNew };
|
|
8715
8756
|
});
|
|
8716
8757
|
|
|
8717
8758
|
/* MT5 sembol seçici: terminaldeki tüm semboller (isteğe bağlı *filter*) */
|
|
@@ -8725,7 +8766,17 @@ ipcMain.handle('finance:settings', async (_e, patch) => {
|
|
|
8725
8766
|
const p = patch || {};
|
|
8726
8767
|
if (p.symbols !== undefined) {
|
|
8727
8768
|
const arr = Array.isArray(p.symbols) ? p.symbols : String(p.symbols).split(/[,\s]+/);
|
|
8728
|
-
|
|
8769
|
+
const next = arr.map((s) => String(s).trim().toUpperCase()).filter(Boolean).slice(0, 20);
|
|
8770
|
+
/* SEMBOL GEÇMİŞİ: eklenen/silinen her sembol hatırlanır — izleme listesi
|
|
8771
|
+
tamamen silinse bile semboller geçmişte kalır, seçicide TEK TIKLA
|
|
8772
|
+
tekrar eklenir (brokere search'e gerek kalmaz). */
|
|
8773
|
+
const hist = Array.isArray(f.symbolHistory) ? f.symbolHistory.slice() : [];
|
|
8774
|
+
for (const s of [...(f.symbols || []), ...next]) {
|
|
8775
|
+
const u = String(s).toUpperCase();
|
|
8776
|
+
if (u && !hist.includes(u)) hist.push(u);
|
|
8777
|
+
}
|
|
8778
|
+
f.symbolHistory = hist.slice(-40);
|
|
8779
|
+
f.symbols = next;
|
|
8729
8780
|
}
|
|
8730
8781
|
if (p.intervalSec !== undefined) f.intervalSec = Math.max(30, Math.min(3600, Math.round(Number(p.intervalSec) || 120)));
|
|
8731
8782
|
if (p.maxLot !== undefined) f.maxLot = Math.max(0.01, Math.min(100, Number(p.maxLot) || 0.1));
|
package/src/preload.js
CHANGED
|
@@ -22,6 +22,11 @@ contextBridge.exposeInMainWorld('beast', {
|
|
|
22
22
|
agentsCancel: (id) => ipcRenderer.invoke('agents:cancel', id),
|
|
23
23
|
agentDmsList: () => ipcRenderer.invoke('agent-dms:list'),
|
|
24
24
|
agentDmsClear: () => ipcRenderer.invoke('agent-dms:clear'),
|
|
25
|
+
toolsList: () => ipcRenderer.invoke('tools:list'),
|
|
26
|
+
toolSave: (t) => ipcRenderer.invoke('tools:save', t),
|
|
27
|
+
toolDelete: (id) => ipcRenderer.invoke('tools:delete', id),
|
|
28
|
+
toolRun: (id, args) => ipcRenderer.invoke('tools:run', { id, args }),
|
|
29
|
+
toolsOpenFolder: () => ipcRenderer.invoke('tools:openFolder'),
|
|
25
30
|
getCeoMode: () => ipcRenderer.invoke('ceo:get'),
|
|
26
31
|
setCeoMode: (v) => ipcRenderer.invoke('ceo:set', v),
|
|
27
32
|
listNotes: () => ipcRenderer.invoke('notes:list'),
|
package/src/renderer/index.html
CHANGED
|
@@ -22,6 +22,16 @@
|
|
|
22
22
|
</div>
|
|
23
23
|
<div id="watchPaneList"></div>
|
|
24
24
|
</div>
|
|
25
|
+
<div id="toolBox">
|
|
26
|
+
<div id="toolBoxHead">
|
|
27
|
+
<span id="toolBoxTitle">TOOLLAR</span>
|
|
28
|
+
<span class="bot-bar-btns">
|
|
29
|
+
<button id="toolFolderBtn" title="Toollar klasörünü aç (%APPDATA%\beast\tools)"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z"/></svg></button>
|
|
30
|
+
<button id="toolAdd" title="Yeni tool yaz">+</button>
|
|
31
|
+
</span>
|
|
32
|
+
</div>
|
|
33
|
+
<div id="toolList"></div>
|
|
34
|
+
</div>
|
|
25
35
|
<div id="botBar">
|
|
26
36
|
<div id="botBarHead">
|
|
27
37
|
<span id="botBarTitle" data-i18n="bot_head">BOTLAR</span>
|
|
@@ -173,6 +183,7 @@
|
|
|
173
183
|
<div class="fin-card-title">
|
|
174
184
|
PİYASA
|
|
175
185
|
<span class="file-spacer"></span>
|
|
186
|
+
<button id="finWatchClear" title="İzleme listesini TAMAMEN sil — semboller geçmişe düşer, sonra tek tıkla geri eklenir">✕︎</button>
|
|
176
187
|
<button id="finSymAdd" title="Sembol ekle/çıkar — MT5 sembol listesinden seç">+</button>
|
|
177
188
|
</div>
|
|
178
189
|
<div id="finSymList"><div class="fin-empty">—</div></div>
|
|
@@ -362,10 +373,39 @@
|
|
|
362
373
|
</aside>
|
|
363
374
|
</div>
|
|
364
375
|
|
|
376
|
+
<div id="toolOverlay" hidden>
|
|
377
|
+
<div id="toolDialog">
|
|
378
|
+
<div id="toolDialogHead">
|
|
379
|
+
<span id="toolDialogTitle">YENİ TOOL</span>
|
|
380
|
+
<button id="toolClose" title="Kapat">✕︎</button>
|
|
381
|
+
</div>
|
|
382
|
+
<div class="tool-form">
|
|
383
|
+
<label>Tool adı (ajan tool__<ad> olarak çağırır)</label>
|
|
384
|
+
<input id="toolName" placeholder="örn: gold_emir" spellcheck="false" autocomplete="off" />
|
|
385
|
+
<label>Açıklama — ajan bu metni görüp ne zaman çağıracağını anlar</label>
|
|
386
|
+
<input id="toolDesc" placeholder="örn: GOLD için özel emir açar (side, lot, sl, tp)" spellcheck="false" autocomplete="off" />
|
|
387
|
+
<label>Parametre şeması (JSON)</label>
|
|
388
|
+
<textarea id="toolParams" rows="6" spellcheck="false" placeholder='{"type":"object","properties":{"side":{"type":"string"},"lot":{"type":"number"}},"required":["side"]}'></textarea>
|
|
389
|
+
<label>run.js kodu — stdin: JSON args · stdout: JSON sonuç (console.log)</label>
|
|
390
|
+
<textarea id="toolCode" rows="12" spellcheck="false" placeholder="const args = JSON.parse(...); console.log(JSON.stringify({ ok: true }));"></textarea>
|
|
391
|
+
<div id="toolFormErr" class="sub" style="color:var(--err)" hidden></div>
|
|
392
|
+
</div>
|
|
393
|
+
<div id="toolTestRow">
|
|
394
|
+
<input id="toolTestArgs" placeholder='deneme argümanları (JSON): {"mesaj":"merhaba"}' spellcheck="false" autocomplete="off" />
|
|
395
|
+
<button id="toolTestBtn">▶︎ Çalıştır</button>
|
|
396
|
+
</div>
|
|
397
|
+
<pre id="toolTestOut" hidden></pre>
|
|
398
|
+
<div id="toolDialogBtns">
|
|
399
|
+
<button id="toolDeleteBtn" class="tool-danger" hidden>Sil</button>
|
|
400
|
+
<span class="file-spacer"></span>
|
|
401
|
+
<button id="toolSaveBtn" class="tool-primary">Kaydet</button>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
|
|
365
406
|
<div id="settingsOverlay" hidden>
|
|
366
407
|
<div id="settingsDialog">
|
|
367
|
-
<aside id="setTabs">
|
|
368
|
-
<div class="set-tabs-title" data-i18n="set_title">AYARLAR</div>
|
|
408
|
+
<aside id="setTabs"> <div class="set-tabs-title" data-i18n="set_title">AYARLAR</div>
|
|
369
409
|
<nav class="set-tabs-list">
|
|
370
410
|
<button class="tab active" data-tab="lang" data-i18n="tab_lang">Dil</button>
|
|
371
411
|
<button class="tab" data-tab="provider" data-i18n="tab_provider">Provider</button>
|
package/src/renderer/renderer.js
CHANGED
|
@@ -48,6 +48,22 @@ const els = {
|
|
|
48
48
|
watchBtn: $('#watchBtn'),
|
|
49
49
|
watchPaneList: $('#watchPaneList'),
|
|
50
50
|
watchPaneOpen: $('#watchPaneOpen'),
|
|
51
|
+
toolBoxAdd: $('#toolAdd'),
|
|
52
|
+
toolList: $('#toolList'),
|
|
53
|
+
toolFolderBtn: $('#toolFolderBtn'),
|
|
54
|
+
toolOverlay: $('#toolOverlay'),
|
|
55
|
+
toolClose: $('#toolClose'),
|
|
56
|
+
toolDialogTitle: $('#toolDialogTitle'),
|
|
57
|
+
toolName: $('#toolName'),
|
|
58
|
+
toolDesc: $('#toolDesc'),
|
|
59
|
+
toolParams: $('#toolParams'),
|
|
60
|
+
toolCode: $('#toolCode'),
|
|
61
|
+
toolFormErr: $('#toolFormErr'),
|
|
62
|
+
toolTestArgs: $('#toolTestArgs'),
|
|
63
|
+
toolTestBtn: $('#toolTestBtn'),
|
|
64
|
+
toolTestOut: $('#toolTestOut'),
|
|
65
|
+
toolSaveBtn: $('#toolSaveBtn'),
|
|
66
|
+
toolDeleteBtn: $('#toolDeleteBtn'),
|
|
51
67
|
watchPaneLog: $('#watchPaneLog'),
|
|
52
68
|
cronBtn: $('#cronBtn'),
|
|
53
69
|
watchOverlay: $('#watchOverlay'),
|
|
@@ -150,6 +166,7 @@ const els = {
|
|
|
150
166
|
finInterval: $('#finInterval'),
|
|
151
167
|
finMaxLot: $('#finMaxLot'),
|
|
152
168
|
finSymBtn: $('#finSymBtn'),
|
|
169
|
+
finWatchClear: $('#finWatchClear'),
|
|
153
170
|
finAllow: $('#finAllow'),
|
|
154
171
|
finTraderBtn: $('#finTraderBtn'),
|
|
155
172
|
finTraderDot: $('#finTraderDot'),
|
|
@@ -5109,6 +5126,192 @@ if (els.dmClear) {
|
|
|
5109
5126
|
});
|
|
5110
5127
|
}
|
|
5111
5128
|
|
|
5129
|
+
/* ---------------- TOOLS konsolu (kişisel araçlar) ----------------
|
|
5130
|
+
%APPDATA%\beast\tools\<ad>\tool.json + run.js — kullanıcı kendi araçlarını
|
|
5131
|
+
yazar; finance dahil TÜM oturumlar tool__<ad> olarak çağırır. Sol panelde
|
|
5132
|
+
buradan görüntülenir, yazılır, test edilir, silinir. */
|
|
5133
|
+
|
|
5134
|
+
const toolUi = {
|
|
5135
|
+
tools: [],
|
|
5136
|
+
editId: null,
|
|
5137
|
+
logRows: [],
|
|
5138
|
+
};
|
|
5139
|
+
|
|
5140
|
+
const TOOL_EXAMPLE_CODE =
|
|
5141
|
+
"'use strict';\n" +
|
|
5142
|
+
"// KİŞİSEL TOOL — stdin: JSON args · stdout: JSON sonuç\n" +
|
|
5143
|
+
"const fs = require('fs');\n" +
|
|
5144
|
+
"let raw = '';\n" +
|
|
5145
|
+
"try { raw = fs.readFileSync(0, 'utf8'); } catch {}\n" +
|
|
5146
|
+
"let args = {};\n" +
|
|
5147
|
+
"try { args = JSON.parse(raw || '{}'); } catch {}\n" +
|
|
5148
|
+
"// Buraya kendi işini yaz (MT5 köprüsü, özel API, hesap, sinyal…)\n" +
|
|
5149
|
+
"console.log(JSON.stringify({ ok: true, echo: args }));\n";
|
|
5150
|
+
|
|
5151
|
+
let toolRenderTimer = null;
|
|
5152
|
+
function scheduleToolRender() {
|
|
5153
|
+
if (toolRenderTimer) return;
|
|
5154
|
+
toolRenderTimer = setTimeout(() => {
|
|
5155
|
+
toolRenderTimer = null;
|
|
5156
|
+
renderToolBox();
|
|
5157
|
+
}, 500);
|
|
5158
|
+
}
|
|
5159
|
+
|
|
5160
|
+
async function renderToolBox() {
|
|
5161
|
+
const list = els.toolList;
|
|
5162
|
+
if (!list) return;
|
|
5163
|
+
try { toolUi.tools = (await beast.toolsList()) || []; } catch { toolUi.tools = []; }
|
|
5164
|
+
list.textContent = '';
|
|
5165
|
+
if (!toolUi.tools.length) {
|
|
5166
|
+
const d = document.createElement('div');
|
|
5167
|
+
d.className = 'tool-empty';
|
|
5168
|
+
d.textContent = 'Tool yok — + ile kendi aracını yaz (Finance dahil tüm oturumlarda çalışır)';
|
|
5169
|
+
list.appendChild(d);
|
|
5170
|
+
return;
|
|
5171
|
+
}
|
|
5172
|
+
for (const t of toolUi.tools) {
|
|
5173
|
+
const row = document.createElement('div');
|
|
5174
|
+
row.className = 'tool-row';
|
|
5175
|
+
const last = toolUi.logRows.filter((l) => l.toolId === t.id).slice(-1)[0];
|
|
5176
|
+
const st = last ? (last.ok ? 'ok' : 'err') : 'idle';
|
|
5177
|
+
const tip = last
|
|
5178
|
+
? 'son çalışma ' + dmTime(last.at) + (last.error ? ' · hata: ' + last.error : ' · ' + last.ms + ' ms')
|
|
5179
|
+
: 'hiç çalışmadı';
|
|
5180
|
+
row.innerHTML =
|
|
5181
|
+
'<span class="tool-dot ' + st + '" title="' + escapeHtml(tip) + '"></span>' +
|
|
5182
|
+
'<span class="tool-name" title="' + escapeHtml(t.description || '') + '">' + escapeHtml(t.name) + '</span>' +
|
|
5183
|
+
'<span class="tool-btns">' +
|
|
5184
|
+
'<button class="tb-run" title="Aç & test çalıştır">▶︎</button>' +
|
|
5185
|
+
'<button class="tb-edit" title="Düzenle">✎︎</button>' +
|
|
5186
|
+
'<button class="tb-del" title="Sil">✕︎</button>' +
|
|
5187
|
+
'</span>';
|
|
5188
|
+
row.querySelector('.tb-run').addEventListener('click', () => openToolDialog(t));
|
|
5189
|
+
row.querySelector('.tb-edit').addEventListener('click', () => openToolDialog(t));
|
|
5190
|
+
row.querySelector('.tb-del').addEventListener('click', async () => {
|
|
5191
|
+
if (!(await uiConfirm('"' + t.name + '" toolu silinsin mi? (ajanlar artık çağıramaz)', 'Sil'))) return;
|
|
5192
|
+
const r = await beast.toolDelete(t.id).catch(() => ({ ok: false }));
|
|
5193
|
+
if (r && r.ok) {
|
|
5194
|
+
toast('Tool silindi');
|
|
5195
|
+
renderToolBox();
|
|
5196
|
+
} else toast((r && r.error) || 'Silinemedi');
|
|
5197
|
+
});
|
|
5198
|
+
list.appendChild(row);
|
|
5199
|
+
}
|
|
5200
|
+
}
|
|
5201
|
+
|
|
5202
|
+
function openToolDialog(t) {
|
|
5203
|
+
if (!els.toolOverlay) return;
|
|
5204
|
+
toolUi.editId = t ? t.id : null;
|
|
5205
|
+
els.toolDialogTitle.textContent = t ? 'TOOL · ' + t.name : 'YENİ TOOL';
|
|
5206
|
+
els.toolName.value = t ? t.name : '';
|
|
5207
|
+
els.toolDesc.value = t ? t.description || '' : '';
|
|
5208
|
+
els.toolParams.value = t
|
|
5209
|
+
? JSON.stringify(t.parameters || { type: 'object', properties: {} }, null, 2)
|
|
5210
|
+
: '{\n "type": "object",\n "properties": {},\n "required": []\n}';
|
|
5211
|
+
els.toolCode.value = t ? t.code || TOOL_EXAMPLE_CODE : TOOL_EXAMPLE_CODE;
|
|
5212
|
+
els.toolTestArgs.value = t && t.parameters && Array.isArray(t.parameters.required) && t.parameters.required.length
|
|
5213
|
+
? JSON.stringify(Object.fromEntries(t.parameters.required.map((k) => [k, ''])), null, 1)
|
|
5214
|
+
: '';
|
|
5215
|
+
els.toolTestOut.hidden = true;
|
|
5216
|
+
els.toolTestOut.textContent = '';
|
|
5217
|
+
els.toolFormErr.hidden = true;
|
|
5218
|
+
els.toolDeleteBtn.hidden = !t;
|
|
5219
|
+
els.toolOverlay.hidden = false;
|
|
5220
|
+
els.toolName.focus();
|
|
5221
|
+
}
|
|
5222
|
+
|
|
5223
|
+
function closeToolDialog() {
|
|
5224
|
+
if (els.toolOverlay) els.toolOverlay.hidden = true;
|
|
5225
|
+
}
|
|
5226
|
+
|
|
5227
|
+
function toolErr(msg) {
|
|
5228
|
+
if (els.toolFormErr) {
|
|
5229
|
+
els.toolFormErr.textContent = msg;
|
|
5230
|
+
els.toolFormErr.hidden = !msg;
|
|
5231
|
+
}
|
|
5232
|
+
}
|
|
5233
|
+
|
|
5234
|
+
if (els.toolBoxAdd) els.toolBoxAdd.addEventListener('click', () => openToolDialog(null));
|
|
5235
|
+
if (els.toolFolderBtn) els.toolFolderBtn.addEventListener('click', () => beast.toolsOpenFolder());
|
|
5236
|
+
if (els.toolClose) els.toolClose.addEventListener('click', closeToolDialog);
|
|
5237
|
+
if (els.toolOverlay) {
|
|
5238
|
+
els.toolOverlay.addEventListener('click', (e) => {
|
|
5239
|
+
if (e.target === els.toolOverlay) closeToolDialog();
|
|
5240
|
+
});
|
|
5241
|
+
els.toolOverlay.addEventListener('keydown', (e) => {
|
|
5242
|
+
if (e.key === 'Escape') { e.stopPropagation(); closeToolDialog(); }
|
|
5243
|
+
});
|
|
5244
|
+
}
|
|
5245
|
+
if (els.toolSaveBtn) {
|
|
5246
|
+
els.toolSaveBtn.addEventListener('click', async () => {
|
|
5247
|
+
toolErr('');
|
|
5248
|
+
const name = String(els.toolName.value || '').trim();
|
|
5249
|
+
if (!name) return toolErr('Tool adı gerekli');
|
|
5250
|
+
let parameters = null;
|
|
5251
|
+
try {
|
|
5252
|
+
parameters = JSON.parse(els.toolParams.value || '{}');
|
|
5253
|
+
} catch {
|
|
5254
|
+
return toolErr('Parametre şeması geçerli JSON değil');
|
|
5255
|
+
}
|
|
5256
|
+
const code = String(els.toolCode.value || '').trim();
|
|
5257
|
+
if (!code) return toolErr('run.js kodu boş olamaz');
|
|
5258
|
+
const r = await beast
|
|
5259
|
+
.toolSave({ id: toolUi.editId, name, description: String(els.toolDesc.value || ''), parameters, code })
|
|
5260
|
+
.catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
|
|
5261
|
+
if (r && r.ok) {
|
|
5262
|
+
closeToolDialog();
|
|
5263
|
+
toast('Tool kaydedildi — ajanlar tool__' + r.id + ' olarak çağırabilir (Finance dahil)');
|
|
5264
|
+
renderToolBox();
|
|
5265
|
+
} else {
|
|
5266
|
+
toolErr((r && r.error) || 'Kaydedilemedi');
|
|
5267
|
+
}
|
|
5268
|
+
});
|
|
5269
|
+
}
|
|
5270
|
+
if (els.toolDeleteBtn) {
|
|
5271
|
+
els.toolDeleteBtn.addEventListener('click', async () => {
|
|
5272
|
+
if (!toolUi.editId) return;
|
|
5273
|
+
const t = toolUi.tools.find((x) => x.id === toolUi.editId);
|
|
5274
|
+
if (!(await uiConfirm('"' + ((t && t.name) || toolUi.editId) + '" toolu silinsin mi?', 'Sil'))) return;
|
|
5275
|
+
const r = await beast.toolDelete(toolUi.editId).catch(() => ({ ok: false }));
|
|
5276
|
+
if (r && r.ok) {
|
|
5277
|
+
closeToolDialog();
|
|
5278
|
+
toast('Tool silindi');
|
|
5279
|
+
renderToolBox();
|
|
5280
|
+
} else toast((r && r.error) || 'Silinemedi');
|
|
5281
|
+
});
|
|
5282
|
+
}
|
|
5283
|
+
if (els.toolTestBtn) {
|
|
5284
|
+
els.toolTestBtn.addEventListener('click', async () => {
|
|
5285
|
+
toolErr('');
|
|
5286
|
+
let args = {};
|
|
5287
|
+
try {
|
|
5288
|
+
args = JSON.parse(els.toolTestArgs.value || '{}');
|
|
5289
|
+
} catch {
|
|
5290
|
+
return toolErr('Test argümanları geçerli JSON değil');
|
|
5291
|
+
}
|
|
5292
|
+
const id = toolUi.editId || String(els.toolName.value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '_').replace(/^_+|_+$/g, '');
|
|
5293
|
+
if (!id) return toolErr('Önce tool adı gir');
|
|
5294
|
+
/* önce kaydet (yeni tool ilk testte de diskte olsun) */
|
|
5295
|
+
const sr = await beast
|
|
5296
|
+
.toolSave({
|
|
5297
|
+
id: toolUi.editId,
|
|
5298
|
+
name: String(els.toolName.value || id),
|
|
5299
|
+
description: String(els.toolDesc.value || ''),
|
|
5300
|
+
parameters: (() => {
|
|
5301
|
+
try { return JSON.parse(els.toolParams.value || '{}'); } catch { return null; }
|
|
5302
|
+
})() || { type: 'object', properties: {} },
|
|
5303
|
+
code: String(els.toolCode.value || ''),
|
|
5304
|
+
})
|
|
5305
|
+
.catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
|
|
5306
|
+
if (!sr || !sr.ok) return toolErr((sr && sr.error) || 'Tool kaydedilemedi');
|
|
5307
|
+
els.toolTestOut.hidden = false;
|
|
5308
|
+
els.toolTestOut.textContent = 'çalışıyor…';
|
|
5309
|
+
const r = await beast.toolRun(sr.id, args).catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
|
|
5310
|
+
els.toolTestOut.textContent = JSON.stringify(r, null, 2).slice(0, 8000);
|
|
5311
|
+
renderToolBox();
|
|
5312
|
+
});
|
|
5313
|
+
}
|
|
5314
|
+
|
|
5112
5315
|
function renderAgentsPane() {
|
|
5113
5316
|
const pane = $('#tab-agents');
|
|
5114
5317
|
if (!pane) return;
|
|
@@ -5267,6 +5470,20 @@ function onEvent(ev) {
|
|
|
5267
5470
|
if ($('#dmRail') && !$('#dmRail').classList.contains('dm-hidden')) renderDmRail();
|
|
5268
5471
|
return;
|
|
5269
5472
|
}
|
|
5473
|
+
/* KİŞİSEL TOOL çalışma kaydı — TOOLS konsolunun nokta göstergeleri */
|
|
5474
|
+
if (ev.type === 'tool-log') {
|
|
5475
|
+
toolUi.logRows.push({
|
|
5476
|
+
at: ev.at,
|
|
5477
|
+
tool: ev.tool,
|
|
5478
|
+
toolId: ev.toolId,
|
|
5479
|
+
ok: ev.ok,
|
|
5480
|
+
ms: ev.ms,
|
|
5481
|
+
error: ev.error,
|
|
5482
|
+
});
|
|
5483
|
+
if (toolUi.logRows.length > 60) toolUi.logRows.splice(0, toolUi.logRows.length - 60);
|
|
5484
|
+
scheduleToolRender();
|
|
5485
|
+
return;
|
|
5486
|
+
}
|
|
5270
5487
|
/* OFFLINE MESAJ KUYRUĞU: bağlantı + kuyruk olayları (sessionId filtresinden önce) */
|
|
5271
5488
|
if (ev.type === 'net' || ev.type === 'netQueue') { onNetEvent(ev); return; }
|
|
5272
5489
|
/* Sandbox klonlama progress'i (sessionId'siz global olay) */
|
|
@@ -6414,6 +6631,7 @@ async function init() {
|
|
|
6414
6631
|
};
|
|
6415
6632
|
|
|
6416
6633
|
refreshAgentsPane();
|
|
6634
|
+
renderToolBox(); // TOOLS konsolu: kişisel araçlar sol panelde
|
|
6417
6635
|
|
|
6418
6636
|
els.sendBtn.addEventListener('click', sendCurrent);
|
|
6419
6637
|
els.stopBtn.addEventListener('click', () => {
|
|
@@ -8502,6 +8720,10 @@ function finSymBtnUpdate() {
|
|
|
8502
8720
|
function finTraderInputsSet(cfg) {
|
|
8503
8721
|
if (!cfg) return;
|
|
8504
8722
|
if (!finWatchDirty) finWatchSet(cfg.symbols);
|
|
8723
|
+
/* sembol geçmişi (silinenler dahil) — seçicide "SON SEMBOLLER" olarak çıkar */
|
|
8724
|
+
finSymHist = Array.isArray(cfg.symbolHistory)
|
|
8725
|
+
? cfg.symbolHistory.map((s) => String(s).trim().toUpperCase()).filter(Boolean).slice(0, 40)
|
|
8726
|
+
: [];
|
|
8505
8727
|
const ae = document.activeElement;
|
|
8506
8728
|
if (els.finInterval && ae !== els.finInterval) els.finInterval.value = cfg.intervalSec || 120;
|
|
8507
8729
|
if (els.finMaxLot && ae !== els.finMaxLot) els.finMaxLot.value = cfg.maxLot || 0.1;
|
|
@@ -8611,7 +8833,15 @@ async function setFinanceMode(on) {
|
|
|
8611
8833
|
const brandSub = document.querySelector('#brand .brand-sub');
|
|
8612
8834
|
if (brandSub) brandSub.textContent = on ? 'Finance' : 'Agent';
|
|
8613
8835
|
try { localStorage.setItem('beast.financeMode', on ? '1' : '0'); } catch {}
|
|
8614
|
-
beast.financeMode(!!on, activeId).catch(() => {});
|
|
8836
|
+
const fm = await beast.financeMode(!!on, activeId).catch(() => ({}));
|
|
8837
|
+
/* finance modu açıldı ama aktif oturum NORMAL (örn. WhatsApp sohbeti) ise
|
|
8838
|
+
ASLA o oturumu finance'e çevirme — kendine ÖZEL finance oturumu aç */
|
|
8839
|
+
if (on && fm && fm.needNew) {
|
|
8840
|
+
try {
|
|
8841
|
+
const created = await beast.createSession();
|
|
8842
|
+
if (created && created.id) await openSession(created.id);
|
|
8843
|
+
} catch {}
|
|
8844
|
+
}
|
|
8615
8845
|
/* sohbet geçmişi listesi değişir: finance modunda yalnız finance sohbetleri */
|
|
8616
8846
|
try {
|
|
8617
8847
|
await refreshSessions();
|
|
@@ -8695,6 +8925,7 @@ if (els.finTraderBtn) {
|
|
|
8695
8925
|
ekle/çıkar. PİYASA kartı yalnız seçili sembolleri gösterir. */
|
|
8696
8926
|
|
|
8697
8927
|
let finWatch = []; /* anlık izleme listesi (snapshot cfg'den senkron) */
|
|
8928
|
+
let finSymHist = []; /* sembol geçmişi — silinenler dahil, seçicide tek tıkla geri ekleme */
|
|
8698
8929
|
let finSymSearchTimer = null;
|
|
8699
8930
|
let finSymRefreshTimer = null;
|
|
8700
8931
|
|
|
@@ -8718,12 +8949,41 @@ function finRenderPickRows(rows, q) {
|
|
|
8718
8949
|
if (!els.finSymPick) return;
|
|
8719
8950
|
els.finSymPick.textContent = '';
|
|
8720
8951
|
const term = String(q || '').trim().toUpperCase();
|
|
8952
|
+
/* arama boşken ÜSTTE geçmiş bölümü: silinen semboller TEK TIKLA geri eklenir */
|
|
8953
|
+
if (!term && finSymHist.length) {
|
|
8954
|
+
const sec = document.createElement('div');
|
|
8955
|
+
sec.className = 'fin-sym-sec';
|
|
8956
|
+
sec.textContent = 'SON SEMBOLLER (sildiklerin dahil — tek tıkla geri ekle)';
|
|
8957
|
+
els.finSymPick.appendChild(sec);
|
|
8958
|
+
for (const name of finSymHist) {
|
|
8959
|
+
const row = document.createElement('div');
|
|
8960
|
+
row.className = 'fin-sym-row';
|
|
8961
|
+
const on = finWatch.includes(name);
|
|
8962
|
+
row.innerHTML =
|
|
8963
|
+
'<span class="fsr-check">' + (on ? '✓' : '') + '</span>' +
|
|
8964
|
+
'<span class="fsr-name">' + escapeHtml(name) + '</span>' +
|
|
8965
|
+
'<span class="fsr-desc">' + (on ? 'izlemede' : 'silinmiş — eklemek için tıkla') + '</span>';
|
|
8966
|
+
if (on) row.classList.add('on');
|
|
8967
|
+
row.addEventListener('click', () => {
|
|
8968
|
+
const added = finToggleWatch(name);
|
|
8969
|
+
row.classList.toggle('on', added);
|
|
8970
|
+
row.querySelector('.fsr-check').textContent = added ? '✓' : '';
|
|
8971
|
+
row.querySelector('.fsr-desc').textContent = added ? 'izlemede' : 'silinmiş — eklemek için tıkla';
|
|
8972
|
+
});
|
|
8973
|
+
els.finSymPick.appendChild(row);
|
|
8974
|
+
}
|
|
8975
|
+
const sep = document.createElement('div');
|
|
8976
|
+
sep.className = 'fin-sym-sec';
|
|
8977
|
+
sep.textContent = 'TÜM MT5 SEMBOLLERİ';
|
|
8978
|
+
els.finSymPick.appendChild(sep);
|
|
8979
|
+
}
|
|
8721
8980
|
const matches = rows.filter((r) => {
|
|
8722
8981
|
if (!term) return true;
|
|
8723
8982
|
return (String(r.name || '').toUpperCase().includes(term) || String(r.desc || '').toUpperCase().includes(term));
|
|
8724
8983
|
});
|
|
8725
8984
|
if (els.finSymCount) els.finSymCount.textContent = matches.length ? matches.length + ' sembol' : '';
|
|
8726
8985
|
if (!matches.length) {
|
|
8986
|
+
if (!term && finSymHist.length) return; /* geçmiş zaten gösterildi */
|
|
8727
8987
|
const d = document.createElement('div');
|
|
8728
8988
|
d.className = 'fin-empty';
|
|
8729
8989
|
d.textContent = term ? 'Eşleşme yok' : 'Sembol listesi boş';
|
|
@@ -8788,6 +9048,26 @@ function finSymPickerClose() {
|
|
|
8788
9048
|
}
|
|
8789
9049
|
|
|
8790
9050
|
if (els.finSymAdd) els.finSymAdd.addEventListener('click', finSymPickerOpen);
|
|
9051
|
+
if (els.finWatchClear) {
|
|
9052
|
+
els.finWatchClear.addEventListener('click', async () => {
|
|
9053
|
+
if (!finWatch.length) {
|
|
9054
|
+
toast('İzleme listesi zaten boş');
|
|
9055
|
+
return;
|
|
9056
|
+
}
|
|
9057
|
+
const ok = await uiConfirm(
|
|
9058
|
+
'İzleme listesindeki TÜM semboller silinsin mi? (' + finWatch.join(', ') + ')\nSilinenler geçmişte kalır — sembol seçicide TEK TIKLA geri ekleyebilirsin.',
|
|
9059
|
+
'Tümünü Sil',
|
|
9060
|
+
'Vazgeç'
|
|
9061
|
+
);
|
|
9062
|
+
if (!ok) return;
|
|
9063
|
+
finWatchSet([]);
|
|
9064
|
+
finSaveCfg({ symbols: [] });
|
|
9065
|
+
finSymBtnUpdate();
|
|
9066
|
+
clearTimeout(finSymRefreshTimer);
|
|
9067
|
+
finSymRefreshTimer = setTimeout(() => finSnapshot(), 700);
|
|
9068
|
+
toast('İzleme listesi temizlendi — semboller geçmişte duruyor');
|
|
9069
|
+
});
|
|
9070
|
+
}
|
|
8791
9071
|
if (els.finSymClose) els.finSymClose.addEventListener('click', finSymPickerClose);
|
|
8792
9072
|
if (els.finSymOverlay) {
|
|
8793
9073
|
els.finSymOverlay.addEventListener('click', (e) => {
|
package/src/renderer/style.css
CHANGED
|
@@ -2206,6 +2206,20 @@ body.rail-hidden #dmRailHead { padding-right: 148px; }
|
|
|
2206
2206
|
.fin-sym:hover .fs-agent { visibility: visible; color: var(--accent); }
|
|
2207
2207
|
.fin-sym .fs-agent:hover { background: var(--accent-dim); }
|
|
2208
2208
|
|
|
2209
|
+
/* sembol seçicide bölüm başlıkları (geçmiş / tüm semboller) */
|
|
2210
|
+
.fin-sym-sec {
|
|
2211
|
+
font-size: 10px;
|
|
2212
|
+
font-weight: 800;
|
|
2213
|
+
letter-spacing: 0.6px;
|
|
2214
|
+
color: var(--accent);
|
|
2215
|
+
padding: 8px 4px 3px;
|
|
2216
|
+
border-bottom: 1px solid var(--border);
|
|
2217
|
+
margin-bottom: 3px;
|
|
2218
|
+
white-space: nowrap;
|
|
2219
|
+
overflow: hidden;
|
|
2220
|
+
text-overflow: ellipsis;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2209
2223
|
/* ---------- BOT SİSTEMİ (sol panel alt bölümü + bot sayfası) ---------- */
|
|
2210
2224
|
|
|
2211
2225
|
/* üst bardaki aktif bot rozeti */
|
|
@@ -2260,6 +2274,225 @@ body.rail-hidden #dmRailHead { padding-right: 148px; }
|
|
|
2260
2274
|
letter-spacing: 1.5px;
|
|
2261
2275
|
color: var(--muted);
|
|
2262
2276
|
}
|
|
2277
|
+
|
|
2278
|
+
/* ---------- sol panel: TOOLS konsolu (kişisel araçlar) ---------- */
|
|
2279
|
+
#toolBox {
|
|
2280
|
+
flex: none;
|
|
2281
|
+
max-height: 32%;
|
|
2282
|
+
display: none;
|
|
2283
|
+
flex-direction: column;
|
|
2284
|
+
border-top: 1px solid var(--border);
|
|
2285
|
+
padding: 6px 8px 6px;
|
|
2286
|
+
}
|
|
2287
|
+
body:not(.ide-mode):not(.studio-mode):not(.sandbox-mode) #toolBox { display: flex; }
|
|
2288
|
+
#toolBoxHead {
|
|
2289
|
+
display: flex;
|
|
2290
|
+
align-items: center;
|
|
2291
|
+
justify-content: space-between;
|
|
2292
|
+
padding: 2px 2px 6px;
|
|
2293
|
+
font-size: 10.5px;
|
|
2294
|
+
font-weight: 800;
|
|
2295
|
+
letter-spacing: 1.5px;
|
|
2296
|
+
color: var(--muted);
|
|
2297
|
+
}
|
|
2298
|
+
#toolList {
|
|
2299
|
+
min-height: 40px;
|
|
2300
|
+
max-height: 200px;
|
|
2301
|
+
overflow-y: auto;
|
|
2302
|
+
display: flex;
|
|
2303
|
+
flex-direction: column;
|
|
2304
|
+
gap: 2px;
|
|
2305
|
+
}
|
|
2306
|
+
.tool-empty {
|
|
2307
|
+
color: var(--muted);
|
|
2308
|
+
font-size: 10.5px;
|
|
2309
|
+
line-height: 1.5;
|
|
2310
|
+
padding: 6px 2px;
|
|
2311
|
+
}
|
|
2312
|
+
.tool-row {
|
|
2313
|
+
display: flex;
|
|
2314
|
+
align-items: center;
|
|
2315
|
+
gap: 6px;
|
|
2316
|
+
min-width: 0;
|
|
2317
|
+
padding: 3px 4px;
|
|
2318
|
+
border-radius: 6px;
|
|
2319
|
+
cursor: pointer;
|
|
2320
|
+
color: var(--text);
|
|
2321
|
+
}
|
|
2322
|
+
.tool-row:hover { background: var(--panel2); }
|
|
2323
|
+
.tool-dot {
|
|
2324
|
+
flex: none;
|
|
2325
|
+
width: 7px;
|
|
2326
|
+
height: 7px;
|
|
2327
|
+
border-radius: 50%;
|
|
2328
|
+
background: var(--border);
|
|
2329
|
+
}
|
|
2330
|
+
.tool-dot.ok { background: #43c463; }
|
|
2331
|
+
.tool-dot.err { background: var(--err); }
|
|
2332
|
+
.tool-name {
|
|
2333
|
+
flex: 1;
|
|
2334
|
+
min-width: 0;
|
|
2335
|
+
font-size: 12px;
|
|
2336
|
+
font-weight: 600;
|
|
2337
|
+
white-space: nowrap;
|
|
2338
|
+
overflow: hidden;
|
|
2339
|
+
text-overflow: ellipsis;
|
|
2340
|
+
}
|
|
2341
|
+
.tool-btns {
|
|
2342
|
+
flex: none;
|
|
2343
|
+
display: flex;
|
|
2344
|
+
gap: 2px;
|
|
2345
|
+
visibility: hidden;
|
|
2346
|
+
}
|
|
2347
|
+
.tool-row:hover .tool-btns { visibility: visible; }
|
|
2348
|
+
.tool-btns button {
|
|
2349
|
+
border: none;
|
|
2350
|
+
background: none;
|
|
2351
|
+
color: var(--muted);
|
|
2352
|
+
font-size: 11px;
|
|
2353
|
+
cursor: pointer;
|
|
2354
|
+
padding: 2px 5px;
|
|
2355
|
+
border-radius: 4px;
|
|
2356
|
+
}
|
|
2357
|
+
.tool-btns button:hover { background: var(--accent-dim); color: var(--accent); }
|
|
2358
|
+
.tool-btns .tb-del:hover { color: var(--err); background: rgba(220, 60, 60, 0.12); }
|
|
2359
|
+
|
|
2360
|
+
/* ---------- TOOL editör dialogu ---------- */
|
|
2361
|
+
#toolOverlay {
|
|
2362
|
+
position: fixed;
|
|
2363
|
+
inset: 0;
|
|
2364
|
+
background: rgba(0, 0, 0, 0.45);
|
|
2365
|
+
z-index: 300;
|
|
2366
|
+
display: flex;
|
|
2367
|
+
align-items: center;
|
|
2368
|
+
justify-content: center;
|
|
2369
|
+
}
|
|
2370
|
+
#toolOverlay[hidden] { display: none; }
|
|
2371
|
+
#toolDialog {
|
|
2372
|
+
width: min(760px, 92vw);
|
|
2373
|
+
max-height: 88vh;
|
|
2374
|
+
overflow-y: auto;
|
|
2375
|
+
background: var(--panel);
|
|
2376
|
+
border: 1px solid var(--border);
|
|
2377
|
+
border-radius: 12px;
|
|
2378
|
+
padding: 14px 16px;
|
|
2379
|
+
display: flex;
|
|
2380
|
+
flex-direction: column;
|
|
2381
|
+
gap: 10px;
|
|
2382
|
+
}
|
|
2383
|
+
#toolDialogHead {
|
|
2384
|
+
display: flex;
|
|
2385
|
+
align-items: center;
|
|
2386
|
+
justify-content: space-between;
|
|
2387
|
+
font-size: 13px;
|
|
2388
|
+
font-weight: 800;
|
|
2389
|
+
letter-spacing: 1px;
|
|
2390
|
+
color: var(--text);
|
|
2391
|
+
}
|
|
2392
|
+
#toolDialogHead button {
|
|
2393
|
+
border: none;
|
|
2394
|
+
background: none;
|
|
2395
|
+
color: var(--muted);
|
|
2396
|
+
font-size: 13px;
|
|
2397
|
+
cursor: pointer;
|
|
2398
|
+
padding: 2px 6px;
|
|
2399
|
+
border-radius: 6px;
|
|
2400
|
+
}
|
|
2401
|
+
#toolDialogHead button:hover { color: var(--err); background: rgba(220, 60, 60, 0.1); }
|
|
2402
|
+
.tool-form {
|
|
2403
|
+
display: flex;
|
|
2404
|
+
flex-direction: column;
|
|
2405
|
+
gap: 4px;
|
|
2406
|
+
}
|
|
2407
|
+
.tool-form label {
|
|
2408
|
+
font-size: 10.5px;
|
|
2409
|
+
font-weight: 700;
|
|
2410
|
+
letter-spacing: 0.5px;
|
|
2411
|
+
color: var(--muted);
|
|
2412
|
+
margin-top: 4px;
|
|
2413
|
+
}
|
|
2414
|
+
.tool-form input,
|
|
2415
|
+
.tool-form textarea {
|
|
2416
|
+
width: 100%;
|
|
2417
|
+
background: var(--bg);
|
|
2418
|
+
color: var(--text);
|
|
2419
|
+
border: 1px solid var(--border);
|
|
2420
|
+
border-radius: 8px;
|
|
2421
|
+
padding: 7px 9px;
|
|
2422
|
+
font-size: 12px;
|
|
2423
|
+
font-family: Consolas, monospace;
|
|
2424
|
+
}
|
|
2425
|
+
.tool-form input { font-family: inherit; }
|
|
2426
|
+
.tool-form textarea { resize: vertical; min-height: 60px; }
|
|
2427
|
+
.tool-form input:focus,
|
|
2428
|
+
.tool-form textarea:focus { outline: none; border-color: var(--accent); }
|
|
2429
|
+
#toolTestRow {
|
|
2430
|
+
display: flex;
|
|
2431
|
+
gap: 6px;
|
|
2432
|
+
}
|
|
2433
|
+
#toolTestArgs {
|
|
2434
|
+
flex: 1;
|
|
2435
|
+
background: var(--bg);
|
|
2436
|
+
color: var(--text);
|
|
2437
|
+
border: 1px solid var(--border);
|
|
2438
|
+
border-radius: 8px;
|
|
2439
|
+
padding: 7px 9px;
|
|
2440
|
+
font-size: 12px;
|
|
2441
|
+
font-family: Consolas, monospace;
|
|
2442
|
+
}
|
|
2443
|
+
#toolTestArgs:focus { outline: none; border-color: var(--accent); }
|
|
2444
|
+
#toolTestBtn {
|
|
2445
|
+
border: none;
|
|
2446
|
+
background: var(--accent-dim);
|
|
2447
|
+
color: var(--accent);
|
|
2448
|
+
font-weight: 800;
|
|
2449
|
+
font-size: 12px;
|
|
2450
|
+
border-radius: 8px;
|
|
2451
|
+
padding: 7px 12px;
|
|
2452
|
+
cursor: pointer;
|
|
2453
|
+
}
|
|
2454
|
+
#toolTestBtn:hover { filter: brightness(1.1); }
|
|
2455
|
+
#toolTestOut {
|
|
2456
|
+
margin: 0;
|
|
2457
|
+
max-height: 220px;
|
|
2458
|
+
overflow: auto;
|
|
2459
|
+
background: var(--bg);
|
|
2460
|
+
border: 1px solid var(--border);
|
|
2461
|
+
border-radius: 8px;
|
|
2462
|
+
padding: 8px 10px;
|
|
2463
|
+
font-size: 11px;
|
|
2464
|
+
line-height: 1.5;
|
|
2465
|
+
white-space: pre-wrap;
|
|
2466
|
+
word-break: break-word;
|
|
2467
|
+
color: var(--text);
|
|
2468
|
+
}
|
|
2469
|
+
#toolDialogBtns {
|
|
2470
|
+
display: flex;
|
|
2471
|
+
align-items: center;
|
|
2472
|
+
gap: 8px;
|
|
2473
|
+
}
|
|
2474
|
+
.tool-primary {
|
|
2475
|
+
border: none;
|
|
2476
|
+
background: var(--accent);
|
|
2477
|
+
color: #fff;
|
|
2478
|
+
font-weight: 800;
|
|
2479
|
+
font-size: 12.5px;
|
|
2480
|
+
border-radius: 8px;
|
|
2481
|
+
padding: 8px 16px;
|
|
2482
|
+
cursor: pointer;
|
|
2483
|
+
}
|
|
2484
|
+
.tool-primary:hover { filter: brightness(1.08); }
|
|
2485
|
+
.tool-danger {
|
|
2486
|
+
border: none;
|
|
2487
|
+
background: rgba(220, 60, 60, 0.12);
|
|
2488
|
+
color: var(--err);
|
|
2489
|
+
font-weight: 700;
|
|
2490
|
+
font-size: 12px;
|
|
2491
|
+
border-radius: 8px;
|
|
2492
|
+
padding: 8px 14px;
|
|
2493
|
+
cursor: pointer;
|
|
2494
|
+
}
|
|
2495
|
+
.tool-danger:hover { background: rgba(220, 60, 60, 0.2); }
|
|
2263
2496
|
.bot-bar-btns { display: flex; gap: 4px; }
|
|
2264
2497
|
.bot-bar-btns button {
|
|
2265
2498
|
border: none; background: none; color: var(--muted);
|