neuroalpha 0.6.13 → 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 -2
- package/server/config.js +73 -53
- package/server/index.js +186 -212
- package/server/llm/local-engine.js +843 -0
- package/server/llm/providers.js +45 -34
- 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'];
|
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,212 +1,186 @@
|
|
|
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, PORT } from './config.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
25
|
-
import { chatState
|
|
26
|
-
import { hub } from './hub.js';
|
|
27
|
-
|
|
28
|
-
markStaleRuns();
|
|
29
|
-
removeLegacyData();
|
|
30
|
-
removeOrphanEvents();
|
|
31
|
-
ensureSeedRun();
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
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
|
-
}
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const ident = IS_INSTANCE ? getIdentity() : null;
|
|
188
|
-
let ver = '';
|
|
189
|
-
try { ver = ` v${JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version}`; } catch { /* ignore */ }
|
|
190
|
-
console.log(`◈ НейроАльфа${ver} запущена${IS_INSTANCE ? ` (${INSTANCE_KIND}) · машина ${ident.id || 'не активирована'} · центр ${ident.centralUrl}` : (IS_CENTRAL ? ' · центр' : '')}`);
|
|
191
|
-
const llm = activeModelInfo();
|
|
192
|
-
console.log(` ИИ-стратег: ${llm.enabled ? `${llm.provider} · ${llm.model}` : 'выключен (Настройки → Модели ИИ)'}`);
|
|
193
|
-
if (!LISTEN_HOST || LISTEN_HOST === '0.0.0.0') for (const url of lanUrls()) console.log(` В сети: ${fmtClickableUrl(url)}`);
|
|
194
|
-
const clientUrl = `http://localhost:${PORT}`;
|
|
195
|
-
const enterHint = process.stdin?.isTTY && !IS_CENTRAL ? ' (Enter — открыть в браузере)' : '';
|
|
196
|
-
console.log(` Клиент: ${fmtClickableUrl(clientUrl)}${enterHint}`);
|
|
197
|
-
if (shouldOpenBrowser) openBrowser(clientUrl);
|
|
198
|
-
if (process.stdin?.isTTY && !IS_CENTRAL) {
|
|
199
|
-
process.stdin.on('data', () => openBrowser(clientUrl));
|
|
200
|
-
process.stdin.unref?.();
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
function openBrowser(url) {
|
|
205
|
-
const [cmd, args] = process.platform === 'win32' ? ['cmd', ['/c', 'start', '""', url]]
|
|
206
|
-
: process.platform === 'darwin' ? ['open', [url]] : ['xdg-open', [url]];
|
|
207
|
-
spawn(cmd, args, { detached: true, stdio: 'ignore', windowsHide: true, windowsVerbatimArguments: true }).on('error', () => {}).unref();
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const shutdown = () => { live.suspend().finally(() => server.close(() => process.exit(0))); setTimeout(() => process.exit(0), 2000); };
|
|
211
|
-
process.on('SIGINT', shutdown);
|
|
212
|
-
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
|
+
});
|