beast-agent 2.38.4 → 2.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agent/bots.js +66 -0
- package/src/agent/customtools.js +52 -2
- package/src/agent/defaulttools/mt5_barlar2/run.js +188 -0
- package/src/agent/defaulttools/mt5_barlar2/tool.json +23 -0
- package/src/agent/defaulttools/mt5_bekleyen/run.js +100 -0
- package/src/agent/defaulttools/mt5_bekleyen/tool.json +17 -0
- package/src/agent/defaulttools/mt5_durum/run.js +93 -0
- package/src/agent/defaulttools/mt5_durum/tool.json +10 -0
- package/src/agent/defaulttools/mt5_emir/run.js +98 -0
- package/src/agent/defaulttools/mt5_emir/tool.json +17 -0
- package/src/agent/defaulttools/mt5_emir_iptal/run.js +90 -0
- package/src/agent/defaulttools/mt5_emir_iptal/tool.json +12 -0
- package/src/agent/defaulttools/mt5_fiyat/run.js +87 -0
- package/src/agent/defaulttools/mt5_fiyat/tool.json +16 -0
- package/src/agent/defaulttools/mt5_gecmis/run.js +87 -0
- package/src/agent/defaulttools/mt5_gecmis/tool.json +12 -0
- package/src/agent/defaulttools/mt5_kapat/run.js +92 -0
- package/src/agent/defaulttools/mt5_kapat/tool.json +13 -0
- package/src/agent/defaulttools/mt5_m15_m5/m15m5.py +60 -0
- package/src/agent/defaulttools/mt5_m15_m5/run.js +73 -0
- package/src/agent/defaulttools/mt5_m15_m5/tool.json +10 -0
- package/src/agent/defaulttools/mt5_pozisyon_gecmis/run.js +144 -0
- package/src/agent/defaulttools/mt5_pozisyon_gecmis/tool.json +12 -0
- package/src/agent/defaulttools/mt5_shot/run.js +428 -0
- package/src/agent/defaulttools/mt5_shot/tool.json +20 -0
- package/src/agent/defaulttools/mt5_sltp/run.js +92 -0
- package/src/agent/defaulttools/mt5_sltp/tool.json +14 -0
- package/src/agent/engine.js +259 -47
- package/src/agent/financetools.js +52 -0
- package/src/agent/mt5/BeastFinance.mq5 +427 -0
- package/src/agent/mt5/BeastShot.tpl +0 -0
- package/src/agent/mt5setup.js +542 -0
- package/src/agent/scripts/mt5_bridge.py +112 -0
- package/src/agent/seeds/mql5/SKILL.md +27 -2
- package/src/agent/seeds/tool-yazma/SKILL.md +6 -4
- package/src/main.js +233 -13
- package/src/preload.js +1 -0
- package/src/renderer/index.html +18 -5
- package/src/renderer/renderer.js +170 -37
- package/src/renderer/style.css +45 -17
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* mt5_m15_m5 - PARAMETRE ALMAZ (argumansiz yedek yol).
|
|
3
|
+
C:\Python314\python.exe ile yanindaki m15m5.py dosyasini calistirir,
|
|
4
|
+
stdout'a TEK satir JSON basar. SALT-OKUNUR: hicbir islem/pending/close cagrisi yok.
|
|
5
|
+
Cikti alanlari: ok, server_time, bid, ask, spread, m15[12], m5[12] -> [epoch,o,h,l,c]
|
|
6
|
+
stdout sert sinir: 2000 bayt. */
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { spawnSync } = require('child_process');
|
|
10
|
+
function resolvePython() {
|
|
11
|
+
const cands = [];
|
|
12
|
+
if (process.env.BEAST_PYTHON) cands.push({ exe: String(process.env.BEAST_PYTHON), args: [] });
|
|
13
|
+
if (process.env.APPDATA) cands.push({ exe: require('path').join(process.env.APPDATA, 'beast', 'py', 'python.exe'), args: [] });
|
|
14
|
+
for (const v of ['314', '313', '312', '311', '310']) cands.push({ exe: 'C:\\Python' + v + '\\python.exe', args: [] });
|
|
15
|
+
cands.push({ exe: 'python', args: [] }, { exe: 'py', args: ['-3'] });
|
|
16
|
+
for (const c of cands) {
|
|
17
|
+
if (c.exe === 'python' || c.exe === 'py') return c;
|
|
18
|
+
try { if (fs.existsSync(c.exe)) return c; } catch (e) {}
|
|
19
|
+
}
|
|
20
|
+
return { exe: 'python', args: [] };
|
|
21
|
+
}
|
|
22
|
+
const PYSPEC = resolvePython();
|
|
23
|
+
const PY = PYSPEC.exe;
|
|
24
|
+
const PY_ARGS = PYSPEC.args;
|
|
25
|
+
|
|
26
|
+
const SCRIPT = path.join(__dirname, 'm15m5.py');
|
|
27
|
+
const LIMIT = 2000;
|
|
28
|
+
|
|
29
|
+
function emit(o, code) {
|
|
30
|
+
process.stdout.write(JSON.stringify(o));
|
|
31
|
+
process.exit(code || 0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
(function main() {
|
|
35
|
+
// parametre almaz: stdin'i beklemeden sessizce yut, araclar arasi kalmasin
|
|
36
|
+
try {
|
|
37
|
+
process.stdin.on('error', () => {});
|
|
38
|
+
process.stdin.on('data', () => {});
|
|
39
|
+
process.stdin.resume();
|
|
40
|
+
} catch { }
|
|
41
|
+
|
|
42
|
+
if (!fs.existsSync(SCRIPT)) return emit({ ok: false, error: 'm15m5.py yok: ' + SCRIPT }, 1);
|
|
43
|
+
|
|
44
|
+
let r;
|
|
45
|
+
try {
|
|
46
|
+
r = spawnSync(PY, [...PY_ARGS, SCRIPT], { encoding: 'utf8', timeout: 60000, windowsHide: true });
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return emit({ ok: false, error: 'python baslatilamadi: ' + String((e && e.message) || e).slice(0, 140) }, 1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const out = String((r && r.stdout) || '').trim();
|
|
52
|
+
if (out) {
|
|
53
|
+
let parsed = null;
|
|
54
|
+
try { parsed = JSON.parse(out); } catch { }
|
|
55
|
+
if (parsed && typeof parsed === 'object') {
|
|
56
|
+
const packed = JSON.stringify(parsed);
|
|
57
|
+
const bytes = Buffer.byteLength(packed, 'utf8');
|
|
58
|
+
if (bytes > LIMIT) return emit({ ok: false, error: 'cikti siniri asildi', bytes: bytes, limit: LIMIT }, 1);
|
|
59
|
+
return emit(parsed, parsed.ok ? 0 : 1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
emit({
|
|
64
|
+
ok: false,
|
|
65
|
+
error: 'python cikti yok/kirik',
|
|
66
|
+
exit_code: r && r.status,
|
|
67
|
+
stderr: String((r && r.stderr) || '').trim().slice(0, 300),
|
|
68
|
+
}, 1);
|
|
69
|
+
})();
|
|
70
|
+
|
|
71
|
+
/* PATCH 2026-09-10b: electron app-mode cocuk prosesi kapanmiyordu (90sn timeout).
|
|
72
|
+
stdout'un boruya bosalmasi icin kisa bekleme, sonra zorla cik. */
|
|
73
|
+
setTimeout(() => process.exit(0), 1500);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mt5_m15_m5",
|
|
3
|
+
"description": "PARAMETRE ALMAZ (argumansiz cagrilir). SALT-OKUNUR, islem acmaz. C:\\Python314\\python.exe ile MetaTrader5 modulunu DOGRUDAN kullanip GOLD icin tek satir JSON dondurur: server_time (HH:MM), bid, ask, spread, m15 (son 12 M15 bari), m5 (son 12 M5 bari) - barlar [epoch,o,h,l,c] biciminde eskiden yeniye. Amac: parametre gecirmeyen runner icin parametresiz yedek yol.",
|
|
4
|
+
"parameters": {
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {},
|
|
7
|
+
"required": []
|
|
8
|
+
},
|
|
9
|
+
"updatedAt": "2026-09-10T10:05:00.000Z"
|
|
10
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// MT5 POZISYON BAZLI GECMIS — açılış+kapanış tek satırda birleşik.
|
|
3
|
+
// stdin: JSON args · stdout: JSON sonuç (SADECE JSON yaz!)
|
|
4
|
+
const { spawn } = require('child_process');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
/* TAŞINABİLİR YOL ÇÖZÜMÜ: Beast gömülü python → sistem python'ları → PATH.
|
|
9
|
+
Köprü dosyası %APPDATA%\beast\finance\mt5_bridge.py'den (veya BEAST_FINANCE_DIR)
|
|
10
|
+
okunur; ortam değişkenleriyle geçersiz kılınabilir. */
|
|
11
|
+
function resolvePython() {
|
|
12
|
+
const cands = [];
|
|
13
|
+
if (process.env.BEAST_PYTHON) cands.push({ exe: String(process.env.BEAST_PYTHON), args: [] });
|
|
14
|
+
if (process.env.APPDATA) cands.push({ exe: path.join(process.env.APPDATA, 'beast', 'py', 'python.exe'), args: [] });
|
|
15
|
+
for (const v of ['314', '313', '312', '311', '310']) cands.push({ exe: 'C:\\Python' + v + '\\python.exe', args: [] });
|
|
16
|
+
cands.push({ exe: 'python', args: [] }, { exe: 'py', args: ['-3'] });
|
|
17
|
+
for (const c of cands) {
|
|
18
|
+
if (c.exe === 'python' || c.exe === 'py') return c;
|
|
19
|
+
try { if (fs.existsSync(c.exe)) return c; } catch (e) {}
|
|
20
|
+
}
|
|
21
|
+
return { exe: 'python', args: [] };
|
|
22
|
+
}
|
|
23
|
+
function resolveBridge() {
|
|
24
|
+
const cands = [];
|
|
25
|
+
if (process.env.BEAST_BRIDGE) cands.push(String(process.env.BEAST_BRIDGE));
|
|
26
|
+
if (process.env.BEAST_FINANCE_DIR) cands.push(path.join(String(process.env.BEAST_FINANCE_DIR), 'mt5_bridge.py'));
|
|
27
|
+
if (process.env.APPDATA) cands.push(path.join(process.env.APPDATA, 'beast', 'finance', 'mt5_bridge.py'));
|
|
28
|
+
for (const p of cands) { try { if (p && fs.existsSync(p)) return p; } catch (e) {} }
|
|
29
|
+
return cands[0] || '';
|
|
30
|
+
}
|
|
31
|
+
const PYSPEC = resolvePython();
|
|
32
|
+
const PY = PYSPEC.exe;
|
|
33
|
+
const PY_ARGS = PYSPEC.args;
|
|
34
|
+
const BRIDGE = resolveBridge();
|
|
35
|
+
|
|
36
|
+
const TIMEOUT = 35000;
|
|
37
|
+
|
|
38
|
+
function call(requests) {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
let py;
|
|
41
|
+
try { py = spawn(PY, [...PY_ARGS, BRIDGE], { windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] }); }
|
|
42
|
+
catch (e) { return resolve({ ok: false, error: 'python baslatilamadi: ' + e.message }); }
|
|
43
|
+
const results = {};
|
|
44
|
+
let buf = '';
|
|
45
|
+
let errTail = '';
|
|
46
|
+
let settled = false;
|
|
47
|
+
const finish = (obj) => { if (!settled) { settled = true; clearTimeout(timer); try { py.kill(); } catch (e) {} resolve(obj); } };
|
|
48
|
+
const timer = setTimeout(() => finish({ ok: false, error: 'zaman asimi: MT5 terminali acik ve yanit veriyor mu?' }), TIMEOUT);
|
|
49
|
+
py.stdout.on('data', (d) => {
|
|
50
|
+
buf += d.toString('utf8');
|
|
51
|
+
let i;
|
|
52
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
53
|
+
const line = buf.slice(0, i).trim(); buf = buf.slice(i + 1);
|
|
54
|
+
if (!line) continue;
|
|
55
|
+
let o = null;
|
|
56
|
+
try { o = JSON.parse(line); } catch (e) { continue; }
|
|
57
|
+
if (o && o.event) continue;
|
|
58
|
+
if (o && typeof o.id === 'number' && Object.prototype.hasOwnProperty.call(o, 'ok')) {
|
|
59
|
+
results[o.id] = o;
|
|
60
|
+
if (Object.keys(results).length >= requests.length) {
|
|
61
|
+
const arr = requests.map((_, k) => results[k + 1]);
|
|
62
|
+
finish(arr.length === 1 ? arr[0] : { ok: true, results: arr });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
py.stderr.on('data', (d) => { errTail = (errTail + d.toString('utf8')).slice(-800); });
|
|
68
|
+
py.on('error', (e) => finish({ ok: false, error: 'python baslatilamadi: ' + e.message }));
|
|
69
|
+
py.on('close', (code) => { if (Object.keys(results).length < requests.length) finish({ ok: false, error: 'bridge erken cikti code=' + code + ' ' + errTail }); });
|
|
70
|
+
requests.forEach((r, k) => py.stdin.write(JSON.stringify({ id: k + 1, method: r.method, params: r.params || {} }) + '\n'));
|
|
71
|
+
py.stdin.end();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const r2 = (n) => Math.round((Number(n) || 0) * 100) / 100;
|
|
76
|
+
const fmt = (t) => {
|
|
77
|
+
if (!t) return '';
|
|
78
|
+
const d = new Date(Number(t) * 1000);
|
|
79
|
+
const p = (x) => String(x).padStart(2, '0');
|
|
80
|
+
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes());
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
(async function main() {
|
|
84
|
+
let args = {};
|
|
85
|
+
try { args = JSON.parse(fs.readFileSync(0, 'utf8') || '{}'); } catch (e) {}
|
|
86
|
+
let days = Number(args.days || 1);
|
|
87
|
+
if (!(days > 0)) days = 1;
|
|
88
|
+
days = Math.min(days, 90);
|
|
89
|
+
const r = await call([{ method: 'deals', params: { days: days } }]);
|
|
90
|
+
if (!r.ok || !r.data) return console.log(JSON.stringify({ ok: false, error: r.error || 'gecmis alinamadi' }));
|
|
91
|
+
|
|
92
|
+
const deals = r.data.deals || [];
|
|
93
|
+
if (!deals.length) return console.log(JSON.stringify({ ok: true, count: 0, positions: [], summary: { net: 0, wins: 0, losses: 0 } }));
|
|
94
|
+
|
|
95
|
+
// Ayni position_id -> tek pozisyon. INOUT (entry=2) deal'ler kendi basina pozisyon.
|
|
96
|
+
const byPos = new Map();
|
|
97
|
+
for (const dl of deals) {
|
|
98
|
+
const pid = (dl.position_id != null && dl.position_id !== 0) ? dl.position_id : dl.ticket;
|
|
99
|
+
if (!byPos.has(pid)) byPos.set(pid, []);
|
|
100
|
+
byPos.get(pid).push(dl);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const positions = [];
|
|
104
|
+
for (const [pid, arr] of byPos) {
|
|
105
|
+
arr.sort((a, b) => (a.time || 0) - (b.time || 0));
|
|
106
|
+
const first = arr[0];
|
|
107
|
+
const entryDeal = arr.find((x) => x.entry === 0) || first;
|
|
108
|
+
const exitDeal = arr.find((x) => x.entry === 1) || arr[arr.length - 1];
|
|
109
|
+
let profit = 0, commission = 0, swap = 0;
|
|
110
|
+
for (const x of arr) { profit += Number(x.profit) || 0; commission += Number(x.commission) || 0; swap += Number(x.swap) || 0; }
|
|
111
|
+
positions.push({
|
|
112
|
+
position_id: pid,
|
|
113
|
+
symbol: first.symbol,
|
|
114
|
+
side: Number(first.type) === 0 ? 'buy' : 'sell',
|
|
115
|
+
volume: r2(first.volume),
|
|
116
|
+
open_price: entryDeal.price,
|
|
117
|
+
open_time: entryDeal.time,
|
|
118
|
+
open_time_str: fmt(entryDeal.time),
|
|
119
|
+
close_price: exitDeal.price,
|
|
120
|
+
close_time: exitDeal.time,
|
|
121
|
+
close_time_str: fmt(exitDeal.time),
|
|
122
|
+
profit: r2(profit),
|
|
123
|
+
commission: r2(commission),
|
|
124
|
+
swap: r2(swap),
|
|
125
|
+
net: r2(profit + commission + swap),
|
|
126
|
+
deal_count: arr.length
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
positions.sort((a, b) => (b.close_time || 0) - (a.close_time || 0));
|
|
130
|
+
|
|
131
|
+
let net = 0, wins = 0, losses = 0;
|
|
132
|
+
for (const p of positions) { net += p.net; if (p.net > 0) wins++; else if (p.net < 0) losses++; }
|
|
133
|
+
|
|
134
|
+
console.log(JSON.stringify({
|
|
135
|
+
ok: true,
|
|
136
|
+
count: positions.length,
|
|
137
|
+
positions: positions,
|
|
138
|
+
summary: { net: r2(net), wins: wins, losses: losses }
|
|
139
|
+
}));
|
|
140
|
+
})();
|
|
141
|
+
|
|
142
|
+
/* PATCH 2026-09-10b: electron app-mode cocuk prosesi kapanmiyordu (90sn timeout).
|
|
143
|
+
stdout'un boruya bosalmasi icin kisa bekleme, sonra zorla cik. */
|
|
144
|
+
setTimeout(() => process.exit(0), 1500);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mt5_pozisyon_gecmis",
|
|
3
|
+
"description": "Kapanan pozisyon geçmişi — açılış ve kapanış TEK SATIRDA birleşik gösterilir (deal'ler ayrı ayrı değil). Sembol, yön, lot, açılış/kapanış fiyatı ve zamanı, net kâr/zarar (profit+komisyon+swap). days verilmezse bugün (en fazla 90).",
|
|
4
|
+
"parameters": {
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"days": { "type": "number", "description": "Kac gun geriye bakilsin (1-90, varsayilan 1 = bugun)" }
|
|
8
|
+
},
|
|
9
|
+
"required": []
|
|
10
|
+
},
|
|
11
|
+
"updatedAt": "2026-09-09T22:10:00.000Z"
|
|
12
|
+
}
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* mt5_shot — MT5 grafiğinden GERÇEK PNG screenshot (BeastFinance EA "shot" komutu)
|
|
3
|
+
stdin: JSON args -> stdout: SADECE JSON sonuç
|
|
4
|
+
|
|
5
|
+
Girdi : {symbol?, timeframe?, width?, height?, file?, template?, timeoutSec?, outdir?, diag?, embed?}
|
|
6
|
+
Çıktı : {ok:true, path, symbol, timeframe, ...} | {ok:false, err, error, cmdId, filesDir, ...}
|
|
7
|
+
err kodları: no_terminal | ea_offline | cmd_write_fail | ea_no_ack | ea_error | png_yok | timeout
|
|
8
|
+
|
|
9
|
+
Notlar:
|
|
10
|
+
- beast_cmd.json'a {id, ts, cmd, params} yazılır (UTF-16 BOM; EA FILE_UNICODE okur).
|
|
11
|
+
- Ack dosyası POLL edilir; PNG MQL5\Files altında belirene kadar beklenir.
|
|
12
|
+
- EA v1.20 şu şablonu işler: {"id":"..","ts":..,"cmd":"shot","params":{file,width,height,symbol?,timeframe?,template?}}
|
|
13
|
+
symbol/timeframe aktif grafikten farklıysa EA ChartOpen+ChartSetSymbolPeriod ile geçici grafik açar.
|
|
14
|
+
- PNG, send_file'ın engellediği %APPDATA%\beast altından çıkarılıp kullanıcı klasörüne
|
|
15
|
+
(varsayılan <home>\beast_shots) kopyalanır; dönen "path" doğrudan send_file'a verilebilir.
|
|
16
|
+
- GÖRSEL ENJEKSİYONU: başarılı sonuçta PNG data URL olarak __injectImage alanına gömülür;
|
|
17
|
+
engine bunu sonraki turda ajana gösterir (embed:false kapatır) — ajan grafiği görerek analiz eder.
|
|
18
|
+
- Watchdog: araç hiçbir koşulda asılı kalmaz; 80 sn'de {ok:false, err:"timeout"} döner.
|
|
19
|
+
*/
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
const SHOT_DIR = path.join(__dirname, 'shots'); // yedek kopya (Beast altı)
|
|
25
|
+
const PUB_DIR_DEFAULT = path.join(os.homedir(), 'beast_shots'); // send_file edilebilir kopya
|
|
26
|
+
const LOCK_FILE = path.join(__dirname, '.shot.lock');
|
|
27
|
+
const LOCK_STALE_MS = 45000;
|
|
28
|
+
const HARD_MS = 80000;
|
|
29
|
+
const DEFAULT_TIMEOUT_SEC = 20;
|
|
30
|
+
|
|
31
|
+
/* ---------------- temel yardımcılar ---------------- */
|
|
32
|
+
function syncSleep(ms) {
|
|
33
|
+
try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
|
|
34
|
+
catch (e) { const t = Date.now(); while (Date.now() - t < ms) { /* busy wait */ } }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let CTX = { cmdId: null, filesDir: null, started: Date.now() };
|
|
38
|
+
let ARGS = {};
|
|
39
|
+
|
|
40
|
+
function out(o) {
|
|
41
|
+
try { fs.writeSync(1, JSON.stringify(o) + '\n'); }
|
|
42
|
+
catch (e) { try { console.log(JSON.stringify(o)); } catch (x) {} }
|
|
43
|
+
try { clearTimeout(WATCHDOG); } catch (e) {}
|
|
44
|
+
try { releaseLock(); } catch (e) {}
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// her hata yolu TEK yerden geçer: ok:false + err kodu + cmdId + filesDir
|
|
49
|
+
function fail(code, msg, extra) {
|
|
50
|
+
const o = Object.assign({
|
|
51
|
+
ok: false,
|
|
52
|
+
err: code,
|
|
53
|
+
error: msg,
|
|
54
|
+
cmdId: CTX.cmdId,
|
|
55
|
+
filesDir: CTX.filesDir,
|
|
56
|
+
ms: Date.now() - CTX.started
|
|
57
|
+
}, extra || {});
|
|
58
|
+
console.error('[mt5_shot] ' + code + ': ' + msg);
|
|
59
|
+
out(o);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ---------------- kilit (eşzamanlı çağrılar ack'i ezmesin) ---------------- */
|
|
63
|
+
function releaseLock() { try { fs.unlinkSync(LOCK_FILE); } catch (e) {} }
|
|
64
|
+
|
|
65
|
+
function acquireLock(waitMs) {
|
|
66
|
+
const deadline = Date.now() + waitMs;
|
|
67
|
+
for (;;) {
|
|
68
|
+
try { fs.writeFileSync(LOCK_FILE, JSON.stringify({ pid: process.pid, at: Date.now() }), { flag: 'wx' }); return true; }
|
|
69
|
+
catch (e) {
|
|
70
|
+
let st = null;
|
|
71
|
+
try { st = fs.statSync(LOCK_FILE); } catch (x) {
|
|
72
|
+
// kilit kayboldu/kilitli kaldı: kısa bekle, sonsuz döngüye girme
|
|
73
|
+
if (Date.now() >= deadline) return false;
|
|
74
|
+
syncSleep(200);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) { // ölü kilit: devral
|
|
78
|
+
try { fs.unlinkSync(LOCK_FILE); } catch (x) {}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (Date.now() >= deadline) return false;
|
|
82
|
+
syncSleep(400);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* ---------------- MT5 köprüsü ---------------- */
|
|
88
|
+
function findTerminalData() {
|
|
89
|
+
const base = path.join(process.env.APPDATA || '', 'MetaQuotes', 'Terminal');
|
|
90
|
+
if (!fs.existsSync(base)) return null;
|
|
91
|
+
let best = null;
|
|
92
|
+
for (const d of fs.readdirSync(base)) {
|
|
93
|
+
const files = path.join(base, d, 'MQL5', 'Files');
|
|
94
|
+
let fresh = 0;
|
|
95
|
+
for (const f of ['beast_ea.json', 'beast_cmd.json', 'beast_cmd_ack.json']) {
|
|
96
|
+
try {
|
|
97
|
+
const p = path.join(files, f);
|
|
98
|
+
if (fs.existsSync(p)) fresh = Math.max(fresh, fs.statSync(p).mtimeMs);
|
|
99
|
+
} catch (e) {}
|
|
100
|
+
}
|
|
101
|
+
if (!fresh) continue;
|
|
102
|
+
if (!best || fresh > best.m) best = { dir: path.join(base, d), files, m: fresh };
|
|
103
|
+
}
|
|
104
|
+
return best;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readMaybeUtf16(fp) {
|
|
108
|
+
const buf = fs.readFileSync(fp);
|
|
109
|
+
let s;
|
|
110
|
+
if (buf.length >= 2 && buf[0] === 0xFF && buf[1] === 0xFE) s = buf.slice(2).toString('utf16le');
|
|
111
|
+
else s = buf.toString('utf8');
|
|
112
|
+
return s.replace(/^\uFEFF/, '').replace(/\0/g, '');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// EA FILE_UNICODE ile okur -> BOM'lu UTF-16 yaz; rename EBUSY olursa doğrudan yaz
|
|
116
|
+
function writeUtf16(fp, str) {
|
|
117
|
+
const buf = Buffer.concat([Buffer.from([0xFF, 0xFE]), Buffer.from(str, 'utf16le')]);
|
|
118
|
+
const tmp = fp + '.tmp' + process.pid;
|
|
119
|
+
try {
|
|
120
|
+
fs.writeFileSync(tmp, buf);
|
|
121
|
+
fs.renameSync(tmp, fp);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
try { fs.unlinkSync(tmp); } catch (x) {}
|
|
124
|
+
fs.writeFileSync(fp, buf);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function readJson(fp) {
|
|
129
|
+
try { return JSON.parse(readMaybeUtf16(fp)); } catch (e) { return null; }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function heartbeatAge(beatFile) {
|
|
133
|
+
try { return Math.round((Date.now() - fs.statSync(beatFile).mtimeMs) / 1000); } catch (e) { return null; }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// MT5 period kodu -> okunabilir periyot adı (16385 = M15)
|
|
137
|
+
const TF_NAMES = { 1:'M1',2:'M2',3:'M3',4:'M4',5:'M5',6:'M6',10:'M10',12:'M12',15:'M15',20:'M20',30:'M30',
|
|
138
|
+
16385:'H1',16386:'H2',16387:'H3',16388:'H4',16390:'H6',16392:'H8',16396:'H12',16408:'D1',32769:'W1',49153:'MN1' };
|
|
139
|
+
function tfName(p) {
|
|
140
|
+
const n = Number(p);
|
|
141
|
+
return TF_NAMES[n] || (TF_NAMES[Math.round(n)] || String(p == null ? '' : p));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* ack yoksa: kanal sağlam mı? kısa bir ping ile ayırt et */
|
|
145
|
+
function eaPing(filesDir, budgetMs) {
|
|
146
|
+
const id = 'P' + Date.now().toString(36);
|
|
147
|
+
const cmdFile = path.join(filesDir, 'beast_cmd.json');
|
|
148
|
+
const ackFile = path.join(filesDir, 'beast_cmd_ack.json');
|
|
149
|
+
const t0 = Date.now();
|
|
150
|
+
try { writeUtf16(cmdFile, JSON.stringify({ id, ts: Date.now(), cmd: 'ping' })); }
|
|
151
|
+
catch (e) { return { ok: false, error: 'ping yazılamadı: ' + e.message }; }
|
|
152
|
+
const dl = Date.now() + budgetMs;
|
|
153
|
+
while (Date.now() < dl) {
|
|
154
|
+
syncSleep(300);
|
|
155
|
+
const a = readJson(ackFile);
|
|
156
|
+
if (a && a.id === id) return { ok: true, ack: a, ms: Date.now() - t0 };
|
|
157
|
+
}
|
|
158
|
+
return { ok: false, error: 'ping yanıtı yok (' + Math.round((Date.now() - t0) / 1000) + ' sn)' };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* ---------------- ana akış ---------------- */
|
|
162
|
+
function run() {
|
|
163
|
+
const timeoutSec = Number(ARGS.timeoutSec) > 0
|
|
164
|
+
? Math.max(5, Math.min(70, Math.round(Number(ARGS.timeoutSec))))
|
|
165
|
+
: DEFAULT_TIMEOUT_SEC;
|
|
166
|
+
const diag = ARGS.diag !== false;
|
|
167
|
+
|
|
168
|
+
const term = findTerminalData();
|
|
169
|
+
if (!term) return fail('no_terminal', 'MT5 terminal veri klasörü bulunamadı (beast_ea.json yok) — MT5 kurulu/çalışıyor mu?');
|
|
170
|
+
const filesDir = term.files;
|
|
171
|
+
CTX.filesDir = filesDir;
|
|
172
|
+
|
|
173
|
+
const cmdFile = path.join(filesDir, 'beast_cmd.json');
|
|
174
|
+
const ackFile = path.join(filesDir, 'beast_cmd_ack.json');
|
|
175
|
+
const beatFile = path.join(filesDir, 'beast_ea.json');
|
|
176
|
+
const beatAge = heartbeatAge(beatFile);
|
|
177
|
+
if (beatAge !== null && beatAge > 60) {
|
|
178
|
+
return fail('ea_offline', 'BeastFinance EA canlı değil (son heartbeat ' + beatAge + ' sn önce) — grafikte EA yüklü ve terminal açık mı?',
|
|
179
|
+
{ heartbeat_age_sec: beatAge });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const locked = acquireLock(Math.min(15000, timeoutSec * 500));
|
|
183
|
+
if (!locked) console.error('[mt5_shot] uyarı: kilit alınamadı, yine de devam ediliyor');
|
|
184
|
+
|
|
185
|
+
const id = 'S' + Date.now().toString(36);
|
|
186
|
+
CTX.cmdId = id;
|
|
187
|
+
|
|
188
|
+
let file = (ARGS.file && String(ARGS.file).trim()) ? String(ARGS.file).trim() : ('beast_shot_' + id + '.png');
|
|
189
|
+
file = path.basename(file); // yol kaçışını engelle
|
|
190
|
+
if (!/\.(png|gif|bmp|jpg|jpeg)$/i.test(file)) file += '.png';
|
|
191
|
+
|
|
192
|
+
const params = { file };
|
|
193
|
+
if (ARGS.timeframe) params.timeframe = String(ARGS.timeframe).toUpperCase();
|
|
194
|
+
if (ARGS.symbol) params.symbol = String(ARGS.symbol).toUpperCase();
|
|
195
|
+
if (ARGS.template !== undefined) params.template = String(ARGS.template);
|
|
196
|
+
params.width = Number(ARGS.width) > 0 ? Math.min(10000, Math.round(Number(ARGS.width))) : 1600;
|
|
197
|
+
params.height = Number(ARGS.height) > 0 ? Math.min(10000, Math.round(Number(ARGS.height))) : 900;
|
|
198
|
+
|
|
199
|
+
const payload = { id, ts: Date.now(), cmd: 'shot', params };
|
|
200
|
+
|
|
201
|
+
try { if (fs.existsSync(ackFile)) fs.unlinkSync(ackFile); } catch (e) {}
|
|
202
|
+
try { writeUtf16(cmdFile, JSON.stringify(payload)); }
|
|
203
|
+
catch (e) { return fail('cmd_write_fail', 'beast_cmd.json yazılamadı: ' + e.message, { sent: payload }); }
|
|
204
|
+
|
|
205
|
+
/* --- ack POLL (cmd dosyasının EA tarafından tüketilmesi de izlenir) --- */
|
|
206
|
+
let ack = null, cmdConsumed = false;
|
|
207
|
+
const ackDeadline = Date.now() + timeoutSec * 1000;
|
|
208
|
+
while (Date.now() < ackDeadline) {
|
|
209
|
+
syncSleep(250);
|
|
210
|
+
const a = readJson(ackFile);
|
|
211
|
+
if (a && a.id === id) { ack = a; break; }
|
|
212
|
+
try {
|
|
213
|
+
if (!fs.existsSync(cmdFile)) cmdConsumed = true;
|
|
214
|
+
else {
|
|
215
|
+
const c = readJson(cmdFile);
|
|
216
|
+
if (c && c.id && c.id !== id) cmdConsumed = true; // başka bir çağrı ezip geçti
|
|
217
|
+
}
|
|
218
|
+
} catch (e) {}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!ack) {
|
|
222
|
+
const extra = {
|
|
223
|
+
heartbeat_age_sec: heartbeatAge(beatFile),
|
|
224
|
+
cmd_consumed: cmdConsumed,
|
|
225
|
+
timeout_sec: timeoutSec,
|
|
226
|
+
sent: payload
|
|
227
|
+
};
|
|
228
|
+
if (diag) {
|
|
229
|
+
extra.diagnostic = cmdConsumed
|
|
230
|
+
? 'EA komutu aldı ama ack yazmadı (shot işleyicisi hata verdi / PNG üretimi bloke)'
|
|
231
|
+
: 'EA komutu hiç almadı (zamanlayıcı durmuş ya da komut dosyası okunamadı)';
|
|
232
|
+
extra.ping = eaPing(filesDir, 6000);
|
|
233
|
+
}
|
|
234
|
+
return fail('ea_no_ack', 'EA ' + timeoutSec + ' sn içinde ack vermedi (id=' + id + ', heartbeat ' +
|
|
235
|
+
(extra.heartbeat_age_sec === null ? '?' : extra.heartbeat_age_sec) + ' sn, cmd tüketildi=' + cmdConsumed + ')', extra);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (ack.ok === false) {
|
|
239
|
+
return fail('ea_error', 'EA hata döndürdü: ' + (ack.error || 'bilinmeyen'), { ack });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* --- PNG dosyasını bekle --- */
|
|
243
|
+
const shotFile = (ack.result && ack.result.file) ? path.basename(String(ack.result.file)) : file;
|
|
244
|
+
const src = path.join(filesDir, shotFile);
|
|
245
|
+
let size = 0;
|
|
246
|
+
const pngDeadline = Math.max(ackDeadline, Date.now() + 8000);
|
|
247
|
+
for (;;) {
|
|
248
|
+
try { if (fs.existsSync(src)) { size = fs.statSync(src).size; if (size > 1500) break; } } catch (e) {}
|
|
249
|
+
if (Date.now() >= pngDeadline) break;
|
|
250
|
+
syncSleep(250);
|
|
251
|
+
}
|
|
252
|
+
if (size <= 1500) {
|
|
253
|
+
return fail('png_yok', 'PNG oluşmadı ya da çok küçük (' + size + ' byte): ' + shotFile,
|
|
254
|
+
{ ack, expected_file: shotFile, bytes: size });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* --- kopyala: Beast altı yedek + send_file edilebilir kullanıcı klasörü --- */
|
|
258
|
+
const pubDir = (ARGS.outdir && String(ARGS.outdir).trim()) ? path.resolve(String(ARGS.outdir).trim()) : PUB_DIR_DEFAULT;
|
|
259
|
+
let pubPath = null, beastPath = null, pubErr = null;
|
|
260
|
+
try {
|
|
261
|
+
if (!fs.existsSync(SHOT_DIR)) fs.mkdirSync(SHOT_DIR, { recursive: true });
|
|
262
|
+
beastPath = path.join(SHOT_DIR, shotFile);
|
|
263
|
+
fs.copyFileSync(src, beastPath);
|
|
264
|
+
} catch (e) { /* yedek kopya zorunlu değil */ }
|
|
265
|
+
try {
|
|
266
|
+
if (!fs.existsSync(pubDir)) fs.mkdirSync(pubDir, { recursive: true });
|
|
267
|
+
pubPath = path.join(pubDir, shotFile);
|
|
268
|
+
fs.copyFileSync(src, pubPath);
|
|
269
|
+
} catch (e) { pubErr = e.message; }
|
|
270
|
+
|
|
271
|
+
const res = (ack.result || {});
|
|
272
|
+
const tfTxt = (res.period ? tfName(res.period) : null) || params.timeframe || null;
|
|
273
|
+
const wantSym = params.symbol || res.symbol || null;
|
|
274
|
+
const mismatch = !!(params.symbol && res.symbol && String(res.symbol).toUpperCase() !== String(params.symbol).toUpperCase());
|
|
275
|
+
|
|
276
|
+
/* --- GÖRSELİ ÇAĞIRAN AJANA GÖSTER: PNG data URL olarak sonuca gömülür;
|
|
277
|
+
engine bunu __injectImage sözleşmesiyle sonraki turda vision mesajı yapar
|
|
278
|
+
(ajan grafiği GERÇEKTEN görür). embed:false ile kapatılır; ~3.5MB üstü
|
|
279
|
+
bağlam/dekont şişmesin diye gömülmez (path yine döner). --- */
|
|
280
|
+
let dataUrl = null;
|
|
281
|
+
try {
|
|
282
|
+
if (ARGS.embed !== false && size > 0 && size <= 3500000) {
|
|
283
|
+
dataUrl = 'data:image/png;base64,' + fs.readFileSync(src).toString('base64');
|
|
284
|
+
}
|
|
285
|
+
} catch (e) {}
|
|
286
|
+
|
|
287
|
+
return out({
|
|
288
|
+
ok: true,
|
|
289
|
+
path: pubPath || beastPath || src, // send_file'a verilecek yol
|
|
290
|
+
path_mt5: src,
|
|
291
|
+
path_beast: beastPath,
|
|
292
|
+
file: shotFile,
|
|
293
|
+
bytes: size,
|
|
294
|
+
image_embedded: !!dataUrl,
|
|
295
|
+
...(dataUrl ? { __injectImage: dataUrl } : {}),
|
|
296
|
+
cmdId: id,
|
|
297
|
+
filesDir: filesDir,
|
|
298
|
+
symbol: res.symbol || wantSym || null,
|
|
299
|
+
timeframe: tfTxt,
|
|
300
|
+
period: res.period || null,
|
|
301
|
+
requested: { symbol: params.symbol || null, timeframe: params.timeframe || null },
|
|
302
|
+
width: params.width,
|
|
303
|
+
height: params.height,
|
|
304
|
+
temp_chart: res.temp_chart === true,
|
|
305
|
+
template_applied: res.template_applied === true,
|
|
306
|
+
symbol_mismatch: mismatch,
|
|
307
|
+
heartbeat_age_sec: heartbeatAge(beatFile),
|
|
308
|
+
ms: Date.now() - CTX.started,
|
|
309
|
+
pubErr: pubErr || undefined
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/* ---------------- stdin / argv (asılmaya karşı sigortalı + çok kanallı) ---------------- */
|
|
314
|
+
function parseArgs(s) {
|
|
315
|
+
const t = String(s || '').trim();
|
|
316
|
+
if (!t) return {};
|
|
317
|
+
try { return JSON.parse(t) || {}; } catch (e) {}
|
|
318
|
+
const m = t.match(/\{[\s\S]*\}/); // kesik/gürültülü girişten kurtar
|
|
319
|
+
if (m) { try { return JSON.parse(m[0]) || {}; } catch (e) {} }
|
|
320
|
+
return {};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// {args:{...}} / {parameters:{...}} gibi sarmalayıcıları düzleştir (her durumda flat args'a in)
|
|
324
|
+
function unwrap(o) {
|
|
325
|
+
let x = (o && typeof o === 'object' && !Array.isArray(o)) ? o : {};
|
|
326
|
+
const WRAP = ['args', 'arguments', 'params', 'parameters', 'input'];
|
|
327
|
+
for (let i = 0; i < 3; i++) {
|
|
328
|
+
const inner = WRAP.map((k) => x[k]).find((v) => v && typeof v === 'object' && !Array.isArray(v));
|
|
329
|
+
if (!inner) break;
|
|
330
|
+
x = Object.assign({}, x, inner);
|
|
331
|
+
for (const k of WRAP) delete x[k];
|
|
332
|
+
}
|
|
333
|
+
if (x.args && typeof x.args === 'object') x = Object.assign({}, x, x.args);
|
|
334
|
+
delete x.args;
|
|
335
|
+
return x;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function argsFromArgv() {
|
|
339
|
+
const av = process.argv.slice(2);
|
|
340
|
+
const joined = av.join(' ').trim();
|
|
341
|
+
if (joined) {
|
|
342
|
+
const j = parseArgs(joined);
|
|
343
|
+
if (Object.keys(j).length) return j;
|
|
344
|
+
}
|
|
345
|
+
const kv = {};
|
|
346
|
+
for (const a of av) {
|
|
347
|
+
const m = /^--?([a-zA-Z_][a-zA-Z0-9_]*)=(.+)$/.exec(a);
|
|
348
|
+
if (m) {
|
|
349
|
+
const v = m[2];
|
|
350
|
+
kv[m[1]] = /^-?\d+(\.\d+)?$/.test(v) ? Number(v) : (v === 'true' ? true : (v === 'false' ? false : v));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return kv;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function loadArgs(rawStdin) {
|
|
357
|
+
const cands = [rawStdin];
|
|
358
|
+
try { cands.push(process.env.BEAST_TOOL_ARGS, process.env.TOOL_ARGS, process.env.TOOL_ARGUMENTS); } catch (e) {}
|
|
359
|
+
const av = argsFromArgv();
|
|
360
|
+
let src = 'none', parsed = {};
|
|
361
|
+
for (const c of cands) {
|
|
362
|
+
if (!c) continue;
|
|
363
|
+
const o = parseArgs(c);
|
|
364
|
+
if (o && Object.keys(o).length) { parsed = o; src = 'stdin/env'; break; }
|
|
365
|
+
}
|
|
366
|
+
if (!src || src === 'none') {
|
|
367
|
+
if (av && Object.keys(av).length) { parsed = av; src = 'argv'; }
|
|
368
|
+
}
|
|
369
|
+
const final = unwrap(parsed);
|
|
370
|
+
// teşhis izi: argüman nereden geldi / hiç geldi mi (asla dışarı gönderilmez)
|
|
371
|
+
try {
|
|
372
|
+
let f0 = null, pcmd = null;
|
|
373
|
+
try {
|
|
374
|
+
const st = fs.fstatSync(0);
|
|
375
|
+
f0 = { isFIFO: st.isFIFO(), isCharacterDevice: st.isCharacterDevice(), isFile: st.isFile(), size: st.size };
|
|
376
|
+
} catch (e) { f0 = { error: String(e && e.message) }; }
|
|
377
|
+
try {
|
|
378
|
+
const { execSync } = require('child_process');
|
|
379
|
+
pcmd = execSync('powershell -NoProfile -Command "(Get-CimInstance Win32_Process -Filter \\"ProcessId=' +
|
|
380
|
+
process.ppid + '\\").CommandLine"', { timeout: 5000 }).toString().trim();
|
|
381
|
+
} catch (e) { pcmd = 'okunamadı: ' + String(e && e.message).slice(0, 120); }
|
|
382
|
+
const envJson = {};
|
|
383
|
+
for (const k of Object.keys(process.env)) {
|
|
384
|
+
if (/key|token|secret|pass|auth|credential/i.test(k)) continue;
|
|
385
|
+
const v = String(process.env[k] || '');
|
|
386
|
+
if (/tool|beast|arg|param|call|json/i.test(k) || v.trim().startsWith('{')) envJson[k] = v.slice(0, 300);
|
|
387
|
+
}
|
|
388
|
+
fs.writeFileSync(path.join(__dirname, 'last-args.json'), JSON.stringify({
|
|
389
|
+
at: new Date().toISOString(), source: src,
|
|
390
|
+
stdin_raw: String(rawStdin || '').slice(0, 1500),
|
|
391
|
+
stdin_fd0: f0, ppid: process.ppid, parent_cmd: pcmd,
|
|
392
|
+
argv: process.argv.slice(2), env_json: envJson, parsed, final
|
|
393
|
+
}, null, 1));
|
|
394
|
+
} catch (e) {}
|
|
395
|
+
if (src === 'none' || !Object.keys(final).length) {
|
|
396
|
+
console.error('[mt5_shot] uyarı: çağrı argümanı alınamadı (stdin/argv boş) — varsayılanlar kullanılıyor');
|
|
397
|
+
}
|
|
398
|
+
return final;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
let raw = '', resolved = false, silenceTimer = null;
|
|
402
|
+
function begin(resolve) { if (resolved) return; resolved = true; resolve(); }
|
|
403
|
+
function finish() {
|
|
404
|
+
ARGS = loadArgs(raw);
|
|
405
|
+
try { run(); }
|
|
406
|
+
catch (e) { fail('timeout', 'beklenmeyen hata: ' + (e && e.message ? e.message : String(e))); }
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
process.stdin.setEncoding('utf8');
|
|
410
|
+
process.stdin.on('data', (d) => {
|
|
411
|
+
raw += d;
|
|
412
|
+
if (silenceTimer) clearTimeout(silenceTimer);
|
|
413
|
+
// JSON tamamlandıysa EOF beklemeden başla; değilse kısa bir sessizlik penceresi daha bekle
|
|
414
|
+
const o = parseArgs(raw);
|
|
415
|
+
if (raw.trim() && Object.keys(o).length) return begin(finish);
|
|
416
|
+
silenceTimer = setTimeout(() => begin(finish), 1200);
|
|
417
|
+
});
|
|
418
|
+
process.stdin.on('end', () => begin(finish));
|
|
419
|
+
process.stdin.on('error', () => begin(finish));
|
|
420
|
+
process.stdin.on('close', () => begin(finish));
|
|
421
|
+
process.stdin.resume();
|
|
422
|
+
setTimeout(() => begin(finish), 2500); // stdin hiç açılmaz/kapanmazsa da devam et (asılmaya izin yok)
|
|
423
|
+
} catch (e) { begin(finish); }
|
|
424
|
+
|
|
425
|
+
/* watchdog: hiçbir koşulda 80 sn üzeri asılı kalma */
|
|
426
|
+
const WATCHDOG = setTimeout(() => {
|
|
427
|
+
fail('timeout', 'mt5_shot ' + Math.round((Date.now() - CTX.started) / 1000) + ' sn içinde tamamlanamadı (watchdog) — ack/PNG beklemesi aşıldı');
|
|
428
|
+
}, HARD_MS);
|