neuroalpha 0.6.12 → 0.7.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/README.md +14 -10
- package/bin/cli.js +79 -69
- package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +2 -2
- package/package.json +4 -5
- package/public/activate.html +41 -19
- package/public/activate.js +81 -10
- package/public/app.js +300 -152
- package/public/index.html +4 -37
- package/server/api/routes.js +570 -906
- package/server/auth/index.js +576 -761
- package/server/brokers/index.js +1 -4
- package/server/config.js +73 -53
- package/server/index.js +186 -187
- package/server/llm/local-engine.js +843 -0
- package/server/llm/providers.js +45 -34
- package/server/model/dataset.js +2 -4
- package/server/model/forecast.js +1 -3
- package/server/news/index.js +2 -3
- package/server/notify/index.js +304 -466
- package/deploy/docs/01-central.md +0 -96
- package/deploy/docs/02-cloud-instances.md +0 -130
- package/deploy/docs/03-desktop-build.md +0 -92
- package/deploy/docs/04-client-guide.md +0 -101
- package/deploy/docs/05-operations.md +0 -109
- package/deploy/docs/06-env-reference.md +0 -91
- package/deploy/docs/07-stress-test.md +0 -253
- package/deploy/docs/pdf/01-central.pdf +0 -0
- package/deploy/docs/pdf/02-cloud-instances.pdf +0 -0
- package/deploy/docs/pdf/03-desktop-build.pdf +0 -0
- package/deploy/docs/pdf/04-client-guide.pdf +0 -0
- package/deploy/docs/pdf/05-operations.pdf +0 -0
- package/deploy/docs/pdf/06-env-reference.pdf +0 -0
- package/deploy/docs/pdf/07-stress-test.pdf +0 -0
- package/deploy/install.sh +0 -128
- package/deploy/k8s/instance.yaml +0 -147
- package/public/admin.html +0 -604
- package/public/landing.html +0 -1207
- package/public/legal.html +0 -134
- package/scripts/set-admin-password.mjs +0 -30
- package/server/billing/yookassa.js +0 -142
- package/server/instance/agent.js +0 -259
- package/server/instance/central.js +0 -514
- package/server/instance/identity.js +0 -79
- package/server/instance/provision.js +0 -166
- package/server/instance/signing.js +0 -71
- package/server/role.js +0 -44
package/server/brokers/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import https from 'node:https';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { ROOT } from '../config.js';
|
|
7
7
|
import { db, kvGet, kvSet, userKvGet, userKvSet, getRun } from '../db.js';
|
|
8
|
-
import { encryptSecret, decryptSecret, currentUser, currentUserId, effectivePlan, runWithUser } from '../auth/index.js';
|
|
8
|
+
import { encryptSecret, decryptSecret, currentUser, currentUserId, effectivePlan, runWithUser, tradingGate } from '../auth/index.js';
|
|
9
9
|
import { notifyBrokerBatch, registerBrokerHandlers } from '../notify/index.js';
|
|
10
10
|
import { getSettings, normalizeParams } from '../settings.js';
|
|
11
11
|
import { latestBarTs, registerRealtimeCandleFetcher } from '../data/moex.js';
|
|
@@ -22,7 +22,6 @@ import { blockingNews } from '../news/index.js';
|
|
|
22
22
|
import { round, MSK, DAY, fmtBarMsk } from '../util.js';
|
|
23
23
|
import { hub } from '../hub.js';
|
|
24
24
|
import { TRUSTED_CA } from '../tls-ca.js';
|
|
25
|
-
import { tradingGate } from '../instance/agent.js';
|
|
26
25
|
|
|
27
26
|
export const FULLY_SUPPORTED_BROKERS = ['tbank'];
|
|
28
27
|
const MANUAL_BROKERS = ['sber', 'vtb', 'alfa'];
|
|
@@ -191,7 +190,6 @@ function logBrokerEvent(acc, kind, text) {
|
|
|
191
190
|
const uid = currentUserId();
|
|
192
191
|
db.prepare('INSERT INTO broker_events (ts, user_id, account_id, kind, text) VALUES (?, ?, ?, ?, ?)')
|
|
193
192
|
.run(Date.now(), uid, String(acc?.id || ''), String(kind), String(text || '').slice(0, 500));
|
|
194
|
-
if (/error|stop|halt/.test(kind)) console.warn(`[брокер ${acc?.name || acc?.id}] ${kind}: ${text}`);
|
|
195
193
|
// храним последние 5000 событий на счёт
|
|
196
194
|
if (Math.random() < 0.02) {
|
|
197
195
|
db.prepare(`DELETE FROM broker_events WHERE user_id = ? AND account_id = ? AND id NOT IN
|
|
@@ -275,7 +273,6 @@ function migrateKvAccounts(uid) {
|
|
|
275
273
|
db.exec('COMMIT');
|
|
276
274
|
} catch (e) { db.exec('ROLLBACK'); throw e; }
|
|
277
275
|
// прежняя запись не удаляется: при откате на версию до 0.4.1 счета останутся на месте (в состоянии на момент переноса)
|
|
278
|
-
console.log(`✓ Брокерские счета пользователя ${uid} перенесены в отдельные записи (${old.length})`);
|
|
279
276
|
}
|
|
280
277
|
userKvSet('brokerAccountsMigrated', true, uid);
|
|
281
278
|
}
|
package/server/config.js
CHANGED
|
@@ -1,53 +1,73 @@
|
|
|
1
|
-
// Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
import { IS_PACKAGED, BUILD } from './build-info.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
1
|
+
// Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { IS_PACKAGED, BUILD } from './build-info.js';
|
|
7
|
+
|
|
8
|
+
export const ROOT = IS_PACKAGED
|
|
9
|
+
? path.resolve(process.env.NEUROALPHA_ROOT || process.cwd())
|
|
10
|
+
: path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
|
+
|
|
12
|
+
// Признак запуска из локального git-репозитория разработчика (вне node_modules / кэша npx)
|
|
13
|
+
const insideNodeModules = /[\\/]node_modules[\\/]|_npx/i.test(ROOT);
|
|
14
|
+
export const IS_DEV_REPO = !IS_PACKAGED && !insideNodeModules && fs.existsSync(path.join(ROOT, '.git'));
|
|
15
|
+
|
|
16
|
+
// Постоянный каталог данных терминала:
|
|
17
|
+
// • при запуске через npx neuroalpha@latest данные ВСЕГДА живут в ~/.neuroalpha и не пропадают при обновлениях;
|
|
18
|
+
// • при локальной разработке из git-репозитория (если есть ./data/trading.db или .git) сохраняется ./data.
|
|
19
|
+
function resolveDataDir() {
|
|
20
|
+
if (process.env.NEUROALPHA_DATA_DIR) return path.resolve(process.env.NEUROALPHA_DATA_DIR);
|
|
21
|
+
const repoData = path.join(ROOT, 'data');
|
|
22
|
+
if (!IS_PACKAGED && !insideNodeModules && (IS_DEV_REPO || fs.existsSync(path.join(repoData, 'trading.db')))) {
|
|
23
|
+
return repoData;
|
|
24
|
+
}
|
|
25
|
+
return path.join(os.homedir(), '.neuroalpha');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const DATA_DIR = resolveDataDir();
|
|
29
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
30
|
+
|
|
31
|
+
if (!IS_PACKAGED) {
|
|
32
|
+
for (const envFile of [path.join(DATA_DIR, '.env'), path.join(ROOT, '.env'), path.resolve(process.cwd(), '.env')]) {
|
|
33
|
+
if (fs.existsSync(envFile)) {
|
|
34
|
+
try { process.loadEnvFile(envFile); } catch { /* ignore */ }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const strategyDefaults = JSON.parse(fs.readFileSync(path.join(ROOT, 'config/strategy.json'), 'utf8'));
|
|
40
|
+
|
|
41
|
+
function resolveApiKey() {
|
|
42
|
+
if (process.env.LLM_API_KEY) return process.env.LLM_API_KEY.trim();
|
|
43
|
+
const file = process.env.LLM_API_KEY_FILE;
|
|
44
|
+
if (!file) return '';
|
|
45
|
+
try {
|
|
46
|
+
const raw = fs.readFileSync(path.resolve(ROOT, file), 'utf8').trim();
|
|
47
|
+
if (raw.startsWith('{')) {
|
|
48
|
+
const j = JSON.parse(raw);
|
|
49
|
+
return String(j.apiKey || j.api_key || j.key || '').trim();
|
|
50
|
+
}
|
|
51
|
+
return raw;
|
|
52
|
+
} catch {
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const llmConfig = {
|
|
58
|
+
provider: (process.env.LLM_PROVIDER || 'openai').toLowerCase(),
|
|
59
|
+
baseUrl: process.env.LLM_BASE_URL || 'https://models.odant.org/v1/chat/completions',
|
|
60
|
+
model: process.env.LLM_MODEL || 'Qwen/Qwen3.8-27B-FP8',
|
|
61
|
+
apiKey: resolveApiKey(),
|
|
62
|
+
reasoningEffort: process.env.LLM_REASONING_EFFORT || 'low',
|
|
63
|
+
maxTokens: Number(process.env.LLM_MAX_TOKENS || 4000),
|
|
64
|
+
timeoutMs: Number(process.env.LLM_TIMEOUT_MS || 90000),
|
|
65
|
+
};
|
|
66
|
+
llmConfig.enabled = llmConfig.provider !== 'none' && !!llmConfig.baseUrl && !!llmConfig.model && !!llmConfig.apiKey;
|
|
67
|
+
|
|
68
|
+
export const PORT = Number(process.env.PORT || 3000);
|
|
69
|
+
export const LISTEN_HOST = String(process.env.HOST || '').trim() || undefined;
|
|
70
|
+
export const DEFAULT_CLOUD_URL = (
|
|
71
|
+
(IS_PACKAGED ? BUILD.centralUrl : (process.env.PORTAL_URL || process.env.CLOUD_URL))
|
|
72
|
+
|| 'https://neuroalpha.odant.org'
|
|
73
|
+
).replace(/\/+$/, '');
|
package/server/index.js
CHANGED
|
@@ -1,187 +1,186 @@
|
|
|
1
|
-
// Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
|
|
2
|
-
import http from 'node:http';
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
&& process.
|
|
152
|
-
&&
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
console.error(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
console.log(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
process.on('SIGTERM', shutdown);
|
|
1
|
+
// Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import express from 'express';
|
|
8
|
+
import { WebSocketServer } from 'ws';
|
|
9
|
+
import { ROOT, DATA_DIR, PORT, LISTEN_HOST, DEFAULT_CLOUD_URL } from './config.js';
|
|
10
|
+
import { activeModelInfo } from './llm/client.js';
|
|
11
|
+
import { registerLocalLlamaProvider } from './llm/providers.js';
|
|
12
|
+
import { setLocalModelReadyHook, resumeSavedLocalEngineOnStartup, getLocalEngineQuickStatus } from './llm/local-engine.js';
|
|
13
|
+
import { markStaleRuns, removeLegacyData, removeOrphanEvents, ensureSeedRun } from './db.js';
|
|
14
|
+
import { createRouter } from './api/routes.js';
|
|
15
|
+
import {
|
|
16
|
+
authMiddleware, activateLocalLicense, userFromRequest, runWithUser,
|
|
17
|
+
getTerminalLicenseState, startAutonomousLicenseWatcher,
|
|
18
|
+
} from './auth/index.js';
|
|
19
|
+
import { securityHeaders, setAppCsp, isCrossOrigin } from './security.js';
|
|
20
|
+
import { live } from './model/live.js';
|
|
21
|
+
import { runningResearch, runningStability } from './model/research.js';
|
|
22
|
+
import { startNews } from './news/index.js';
|
|
23
|
+
import { startNotifyPolling } from './notify/index.js';
|
|
24
|
+
import { startBrokerScheduler } from './brokers/index.js';
|
|
25
|
+
import { chatState } from './strategist/index.js';
|
|
26
|
+
import { hub } from './hub.js';
|
|
27
|
+
|
|
28
|
+
markStaleRuns();
|
|
29
|
+
removeLegacyData();
|
|
30
|
+
removeOrphanEvents();
|
|
31
|
+
ensureSeedRun();
|
|
32
|
+
|
|
33
|
+
setLocalModelReadyHook((info) => {
|
|
34
|
+
registerLocalLlamaProvider(info);
|
|
35
|
+
hub.emit({ type: 'llm:changed', llm: activeModelInfo() });
|
|
36
|
+
hub.emit({ type: 'chat:state', state: chatState() });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const app = express();
|
|
40
|
+
app.disable('x-powered-by');
|
|
41
|
+
app.use(express.json({ limit: '1mb' }));
|
|
42
|
+
app.use(securityHeaders);
|
|
43
|
+
app.use(authMiddleware);
|
|
44
|
+
const server = http.createServer(app);
|
|
45
|
+
const wss = new WebSocketServer({ server, path: '/ws' });
|
|
46
|
+
|
|
47
|
+
hub.setSender((msg, { uid, global }) => {
|
|
48
|
+
const data = JSON.stringify(msg);
|
|
49
|
+
for (const c of wss.clients) {
|
|
50
|
+
if (c.readyState !== 1) continue;
|
|
51
|
+
if (global || c.uid === uid) c.send(data);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
wss.on('connection', (ws, req) => {
|
|
56
|
+
if (isCrossOrigin(req)) {
|
|
57
|
+
ws.close(4403, 'forbidden origin');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const user = userFromRequest(req);
|
|
61
|
+
if (!user) {
|
|
62
|
+
ws.close(4401, 'unauthorized');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
ws.uid = user.id;
|
|
66
|
+
runWithUser(user, () => {
|
|
67
|
+
ws.send(JSON.stringify({
|
|
68
|
+
type: 'hello',
|
|
69
|
+
live: live.status(),
|
|
70
|
+
chat: chatState(),
|
|
71
|
+
running: runningResearch(),
|
|
72
|
+
stabilityRunning: runningStability(),
|
|
73
|
+
llm: activeModelInfo(),
|
|
74
|
+
localLlm: getLocalEngineQuickStatus(),
|
|
75
|
+
}));
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
startAutonomousLicenseWatcher();
|
|
80
|
+
resumeSavedLocalEngineOnStartup();
|
|
81
|
+
live.resume();
|
|
82
|
+
startNews();
|
|
83
|
+
startNotifyPolling();
|
|
84
|
+
startBrokerScheduler();
|
|
85
|
+
|
|
86
|
+
const cliToken = process.env.NEUROALPHA_TOKEN || process.env.NEUROALPHA_API_KEY || '';
|
|
87
|
+
if (cliToken && cliToken.startsWith('na_tok_')) {
|
|
88
|
+
activateLocalLicense(cliToken, process.env.PORTAL_URL || process.env.CLOUD_URL || '')
|
|
89
|
+
.then((lic) => console.log(` ✓ Активирована лицензия: тариф «${lic.planName}» (${lic.email})`))
|
|
90
|
+
.catch((e) => console.warn(` ⚠ Активация токена: ${e.message}`));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
app.get('/healthz', (req, res) => res.json({ ok: true, app: 'neuroalpha' }));
|
|
94
|
+
|
|
95
|
+
app.get(['/legal', '/legal.html'], (req, res) => {
|
|
96
|
+
const portalUrl = getTerminalLicenseState().portalUrl || DEFAULT_CLOUD_URL;
|
|
97
|
+
res.redirect(`${portalUrl}/legal`);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Если терминал ещё не активирован токеном из ЛК ИЛИ открыт по внешней сети/на VPS без сессии — показываем экран активации / входа
|
|
101
|
+
app.get(['/', '/index.html'], (req, res, next) => {
|
|
102
|
+
if (!req.user) {
|
|
103
|
+
setAppCsp(res);
|
|
104
|
+
return res.sendFile(path.join(ROOT, 'public', 'activate.html'));
|
|
105
|
+
}
|
|
106
|
+
setAppCsp(res);
|
|
107
|
+
return next();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
app.use('/api', createRouter());
|
|
111
|
+
app.use('/docs', express.static(path.join(ROOT, 'docs')));
|
|
112
|
+
app.use(express.static(path.join(ROOT, 'public')));
|
|
113
|
+
|
|
114
|
+
function openBrowser(url) {
|
|
115
|
+
try {
|
|
116
|
+
if (process.platform === 'win32') {
|
|
117
|
+
spawn('cmd', ['/c', 'start', '""', url], { detached: true, stdio: 'ignore', windowsHide: true, windowsVerbatimArguments: true }).on('error', () => {}).unref();
|
|
118
|
+
} else if (process.platform === 'darwin') {
|
|
119
|
+
spawn('open', [url], { detached: true, stdio: 'ignore' }).on('error', () => {}).unref();
|
|
120
|
+
} else if (process.env.DISPLAY || process.env.WAYLAND_DISPLAY) {
|
|
121
|
+
spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).on('error', () => {}).unref();
|
|
122
|
+
}
|
|
123
|
+
} catch { /* ignore */ }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function lanUrls() {
|
|
127
|
+
return Object.entries(os.networkInterfaces())
|
|
128
|
+
.filter(([name]) => !/vethernet|wsl|docker|hyper-v|virtualbox|vmware/i.test(name))
|
|
129
|
+
.flatMap(([, list]) => list || [])
|
|
130
|
+
.filter((a) => a && a.family === 'IPv4' && !a.internal)
|
|
131
|
+
.map((a) => `http://${a.address}:${PORT}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function fmtClickableUrl(url, out = process.stdout) {
|
|
135
|
+
if (!out?.isTTY) return url;
|
|
136
|
+
const osc8 = Boolean(
|
|
137
|
+
process.env.WT_SESSION
|
|
138
|
+
|| process.env.TERM_PROGRAM
|
|
139
|
+
|| process.env.COLORTERM
|
|
140
|
+
|| process.env.FORCE_HYPERLINK === '1'
|
|
141
|
+
|| (process.platform !== 'win32' && process.env.TERM && process.env.TERM !== 'dumb'),
|
|
142
|
+
);
|
|
143
|
+
const styled = `\x1b[4;96m${url}\x1b[0m`;
|
|
144
|
+
return osc8 ? `\x1b]8;;${url}\x1b\\${styled}\x1b]8;;\x1b\\` : styled;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const shouldOpenBrowser = process.env.OPEN_BROWSER === '1'
|
|
148
|
+
|| (process.env.OPEN_BROWSER !== '0'
|
|
149
|
+
&& process.env.NO_BROWSER !== '1'
|
|
150
|
+
&& !process.argv.includes('--no-browser')
|
|
151
|
+
&& !process.execArgv.includes('--watch')
|
|
152
|
+
&& (process.platform === 'win32' || process.platform === 'darwin' || Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY)));
|
|
153
|
+
|
|
154
|
+
wss.on('error', () => {});
|
|
155
|
+
server.on('error', (e) => {
|
|
156
|
+
const clientUrl = `http://localhost:${PORT}`;
|
|
157
|
+
if (e.code === 'EADDRINUSE') {
|
|
158
|
+
console.error(`Порт ${PORT} занят: терминал уже запущен? Клиент: ${fmtClickableUrl(clientUrl, process.stderr)}`);
|
|
159
|
+
if (shouldOpenBrowser) openBrowser(clientUrl);
|
|
160
|
+
} else {
|
|
161
|
+
console.error(e);
|
|
162
|
+
}
|
|
163
|
+
process.exit(1);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
server.listen(PORT, LISTEN_HOST, () => {
|
|
167
|
+
let ver = '';
|
|
168
|
+
try { ver = ` v${JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version}`; } catch { /* ignore */ }
|
|
169
|
+
const lic = getTerminalLicenseState();
|
|
170
|
+
const licInfo = lic.activated
|
|
171
|
+
? (lic.devBypass ? 'режим разработчика' : `${lic.planName}${lic.perpetual ? ' (бессрочно)' : ''}`)
|
|
172
|
+
: 'ожидает активации токеном ЛК';
|
|
173
|
+
console.log(`◈ НейроАльфа Терминал${ver} запущен · ${licInfo}`);
|
|
174
|
+
console.log(` Данные: ${DATA_DIR}`);
|
|
175
|
+
const llm = activeModelInfo();
|
|
176
|
+
console.log(` ИИ-стратег: ${llm.enabled ? `${llm.provider} · ${llm.model}` : 'выключен (Настройки → Модели ИИ)'}`);
|
|
177
|
+
if (!LISTEN_HOST || LISTEN_HOST === '0.0.0.0') for (const url of lanUrls()) console.log(` В сети: ${fmtClickableUrl(url)}`);
|
|
178
|
+
const clientUrl = `http://localhost:${PORT}`;
|
|
179
|
+
const enterHint = process.stdin?.isTTY ? ' (Enter — открыть в браузере)' : '';
|
|
180
|
+
console.log(` Клиент: ${fmtClickableUrl(clientUrl)}${enterHint}`);
|
|
181
|
+
if (shouldOpenBrowser) openBrowser(clientUrl);
|
|
182
|
+
if (process.stdin?.isTTY) {
|
|
183
|
+
process.stdin.on('data', () => openBrowser(clientUrl));
|
|
184
|
+
process.stdin.unref?.();
|
|
185
|
+
}
|
|
186
|
+
});
|