neuroalpha 0.2.0 → 0.3.12
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/LICENSE +21 -0
- package/README.md +7 -3
- package/bin/cli.js +69 -69
- package/config/seed-run.json +1 -0
- package/config/strategy.json +4 -10
- package/deploy/install.sh +128 -0
- package/docs//320/224/320/225/320/237/320/233/320/236/320/231.md +103 -0
- package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +1 -1
- package/docs//320/234/320/225/320/245/320/220/320/235/320/230/320/232/320/220_/320/230_/320/237/320/220/320/240/320/220/320/234/320/225/320/242/320/240/320/253.md +171 -0
- package/package.json +15 -5
- package/public/admin.html +426 -0
- package/public/app.js +741 -251
- package/public/index.html +45 -20
- package/public/landing.html +1004 -870
- package/public/legal.html +22 -0
- package/public/styles.css +49 -15
- package/server/api/routes.js +176 -23
- package/server/auth/index.js +544 -340
- package/server/billing/yookassa.js +1 -0
- package/server/brokers/index.js +1224 -645
- package/server/config.js +6 -4
- package/server/data/moex.js +1 -0
- package/server/db.js +53 -2
- package/server/hub.js +1 -0
- package/server/index.js +10 -2
- package/server/llm/client.js +1 -0
- package/server/llm/providers.js +50 -3
- package/server/model/dataset.js +362 -352
- package/server/model/forecast.js +15 -21
- package/server/model/linalg.js +1 -0
- package/server/model/live.js +489 -486
- package/server/model/metrics.js +1 -0
- package/server/model/models.js +31 -21
- package/server/model/profiles.js +1 -0
- package/server/model/research.js +119 -172
- package/server/model/vocab.js +1 -0
- package/server/news/index.js +1 -0
- package/server/notify/index.js +304 -168
- package/server/params-meta.js +20 -25
- package/server/settings.js +1 -0
- package/server/strategist/analytics.js +1 -0
- package/server/strategist/index.js +6 -6
- package/server/strategist/tools.js +2 -1
- package/server/util.js +1 -0
- package/server/model/gpu-worker.html +0 -11
- package/server/model/gpu-worker.js +0 -60
- package/server/model/tf-node.js +0 -26
- package/server/model/transformer-core.js +0 -330
- package/server/model/transformer-gpu.js +0 -221
- package/server/model/transformer-pool.js +0 -200
- package/server/model/transformer-worker.js +0 -28
- package/server/model/transformer.js +0 -57
package/public/legal.html
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<!-- Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License. -->
|
|
1
2
|
<!doctype html>
|
|
2
3
|
<html lang="ru">
|
|
3
4
|
<head>
|
|
@@ -85,6 +86,27 @@
|
|
|
85
86
|
<li>Используя сервис «НейроАльфа», Пользователь подтверждает, что осознаёт указанные риски и принимает все инвестиционные решения самостоятельно.</li>
|
|
86
87
|
</ul>
|
|
87
88
|
</section>
|
|
89
|
+
|
|
90
|
+
<section class="doc" id="requisites">
|
|
91
|
+
<h2>4. Реквизиты Исполнителя и контакты поддержки</h2>
|
|
92
|
+
<div id="legal-req-body">
|
|
93
|
+
<p>Загрузка реквизитов…</p>
|
|
94
|
+
</div>
|
|
95
|
+
</section>
|
|
96
|
+
<div style="text-align:center;color:var(--muted);font-size:13px;margin-top:16px">© 2026 Roman A. Perepelkin · Licensed under the MIT License</div>
|
|
88
97
|
</div>
|
|
98
|
+
<script>
|
|
99
|
+
fetch('/api/public/legal').then((r) => r.json()).then((d) => {
|
|
100
|
+
const rows = [
|
|
101
|
+
d.ownerName ? `<li><b>Исполнитель:</b> ${d.ownerName}${d.status ? ` (${d.status})` : ''}</li>` : '',
|
|
102
|
+
d.inn ? `<li><b>ИНН:</b> ${d.inn}</li>` : '',
|
|
103
|
+
d.ogrn ? `<li><b>ОГРН / ОГРНИП:</b> ${d.ogrn}</li>` : '',
|
|
104
|
+
d.email ? `<li><b>Email технической поддержки:</b> <a href="mailto:${d.email}">${d.email}</a></li>` : '',
|
|
105
|
+
d.phone ? `<li><b>Телефон:</b> ${d.phone}</li>` : '',
|
|
106
|
+
d.address ? `<li><b>Местонахождение:</b> ${d.address}</li>` : '',
|
|
107
|
+
].filter(Boolean).join('');
|
|
108
|
+
document.getElementById('legal-req-body').innerHTML = rows ? `<ul>${rows}</ul>` : '<p>Связь с поддержкой — в личном кабинете сервиса.</p>';
|
|
109
|
+
}).catch(() => {});
|
|
110
|
+
</script>
|
|
89
111
|
</body>
|
|
90
112
|
</html>
|
package/public/styles.css
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License. */
|
|
1
2
|
:root {
|
|
2
3
|
--bg: #0d1016;
|
|
3
4
|
--panel: #13171f;
|
|
@@ -147,7 +148,11 @@ body.light .model-opt:hover:not(:disabled), body.light .model-opt.active { backg
|
|
|
147
148
|
.model-manage-btn:hover { background: #ffffff0c; color: var(--accent); border-color: var(--line); }
|
|
148
149
|
body.light .model-manage-btn:hover { background: #f0f3f6; }
|
|
149
150
|
.pbtn { width: 34px; height: 34px; border-radius: 999px; padding: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 14px; background: var(--bg); border: 1px solid var(--line); color: var(--text); transition: background .15s, border-color .15s, color .15s, transform .15s; flex: none; }
|
|
150
|
-
.pbtn[hidden]
|
|
151
|
+
.pbtn[hidden] { display: none; }
|
|
152
|
+
#chat-action svg { display: none; }
|
|
153
|
+
#chat-action[data-icon="mic"] #ico-mic { display: block; }
|
|
154
|
+
#chat-action[data-icon="send"] #ico-send { display: block; }
|
|
155
|
+
#chat-action[data-icon="stop"] #ico-stop { display: block; }
|
|
151
156
|
.pbtn.action-btn { background: #ffffff0c; border-color: transparent; color: var(--text); }
|
|
152
157
|
body.light .pbtn.action-btn { background: #eef1f5; color: #374151; }
|
|
153
158
|
.pbtn.action-btn:hover:not(:disabled) { border-color: var(--violet); color: var(--violet); }
|
|
@@ -182,12 +187,28 @@ body.light .tbadge.run { background: #dbeafe; border-color: #93c5fd; }
|
|
|
182
187
|
.progress .bar { position: absolute; inset: 0 auto 0 0; width: 0; background: #24467f; transition: width .2s; }
|
|
183
188
|
.progress span { position: relative; font-size: 12px; padding: 0 8px; line-height: 22px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; }
|
|
184
189
|
.empty { margin-top: 30px; color: var(--muted); line-height: 1.7; max-width: 640px; }
|
|
190
|
+
.empty.live-dash { margin-top: 14px; max-width: none; color: var(--text); line-height: 1.45; }
|
|
185
191
|
.empty b { color: var(--text); }
|
|
192
|
+
.ld-grid { display: grid; grid-template-columns: 1.15fr 0.85fr; gap: 16px; margin-top: 12px; }
|
|
193
|
+
@media (max-width: 1100px) { .ld-grid { grid-template-columns: 1fr; } }
|
|
194
|
+
.ld-card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; min-width: 0; }
|
|
195
|
+
.ld-title { font-size: 13px; font-weight: 600; color: var(--text); margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; gap: 8px; }
|
|
196
|
+
.ld-tl { position: relative; height: 26px; background: var(--bg); border: 1px solid var(--line); border-radius: 7px; overflow: hidden; margin: 8px 0 6px; }
|
|
197
|
+
.ld-tl-done { position: absolute; top: 0; bottom: 0; background: rgba(63, 185, 80, 0.16); transition: left .25s, width .25s; }
|
|
198
|
+
.ld-tl-train { position: absolute; top: 2px; bottom: 2px; background: rgba(79, 140, 255, 0.32); border: 1px solid var(--accent); border-radius: 5px; transition: left .25s, width .25s; display: flex; align-items: center; justify-content: center; font-size: 10.5px; font-weight: 600; color: var(--text); overflow: hidden; white-space: nowrap; }
|
|
199
|
+
.ld-tl-test { position: absolute; top: 2px; bottom: 2px; background: rgba(63, 185, 80, 0.42); border: 1px solid var(--green); border-radius: 5px; transition: left .25s, width .25s; }
|
|
200
|
+
.ld-ics { display: flex; align-items: flex-end; gap: 3px; height: 64px; padding-top: 8px; border-bottom: 1px solid var(--line); margin-top: 8px; }
|
|
201
|
+
.ld-ic-bar { flex: 1; min-width: 4px; max-width: 18px; border-radius: 2px 2px 0 0; transition: height .2s; }
|
|
202
|
+
.ld-syms { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 10px; }
|
|
203
|
+
.ld-sym { font-size: 11px; padding: 2px 7px; border-radius: 5px; background: var(--bg); border: 1px solid var(--line); color: var(--text); }
|
|
204
|
+
.ld-sym.ok { border-color: rgba(63, 185, 80, 0.5); color: var(--green); }
|
|
186
205
|
.verdict { margin: 16px 0 12px; font-size: 15px; line-height: 1.5; }
|
|
187
|
-
.kpis { display: grid; grid-template-columns: repeat(
|
|
206
|
+
.kpis { display: grid; grid-template-columns: repeat(5, 1fr); gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
|
|
207
|
+
@media (max-width: 1180px) { .kpis { grid-template-columns: repeat(3, 1fr); } }
|
|
188
208
|
.kpi { background: var(--bg); padding: 10px 14px; cursor: help; }
|
|
189
|
-
.kpi .v { font-size:
|
|
190
|
-
.kpi .l { font-size: 12px; color: var(--muted); }
|
|
209
|
+
.kpi .v { font-size: 21px; font-weight: 600; line-height: 1.2; }
|
|
210
|
+
.kpi .l { font-size: 12px; color: var(--muted); margin-top: 2px; }
|
|
211
|
+
.kpi .sub { font-size: 11.5px; color: var(--muted); margin-top: 2px; line-height: 1.3; }
|
|
191
212
|
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: 6px; overflow: hidden; margin: 14px 0 6px; }
|
|
192
213
|
.seg button { border: 0; border-radius: 0; padding: 3px 10px; font-size: 12px; color: var(--muted); }
|
|
193
214
|
.seg button.active { background: var(--panel); color: var(--text); }
|
|
@@ -258,27 +279,29 @@ body.light input[type=checkbox].switch:checked { background: #1a7f37; border-col
|
|
|
258
279
|
.headbar .b { height: 6px; background: var(--panel); border-radius: 3px; position: relative; }
|
|
259
280
|
.headbar .b div { position: absolute; top: 0; bottom: 0; border-radius: 3px; }
|
|
260
281
|
|
|
261
|
-
.settings-wrap { display: grid; grid-template-columns:
|
|
262
|
-
|
|
282
|
+
.settings-wrap { display: grid; grid-template-columns: 205px minmax(0, 740px); gap: 24px; align-items: start; min-width: 0; }
|
|
283
|
+
@media (max-width: 1100px) { .settings-wrap { grid-template-columns: 180px minmax(0, 1fr); gap: 16px; } }
|
|
284
|
+
.snav { display: flex; flex-direction: column; gap: 3px; position: sticky; top: 0; min-width: 0; }
|
|
263
285
|
.snav #s-search { width: 100%; margin-bottom: 10px; background: transparent; border-color: var(--line); padding: 6px 10px; font-size: 13px; }
|
|
264
286
|
.snav button { text-align: left; border: 0; padding: 7px 12px; border-radius: 6px; color: var(--muted); font-size: 13.5px; }
|
|
265
287
|
.snav button:hover { background: #ffffff08; color: var(--text); }
|
|
266
288
|
.snav button.active { background: #262b36; color: var(--text); font-weight: 500; }
|
|
267
289
|
.snav .ssep { height: 1px; background: var(--line); margin: 8px 4px; }
|
|
268
290
|
.snav .sreset { color: var(--muted); font-size: 12.5px; }
|
|
269
|
-
.spane { min-width: 0; }
|
|
270
|
-
.ssec { margin-bottom: 24px; }
|
|
291
|
+
.spane { min-width: 0; overflow: hidden; }
|
|
292
|
+
.ssec { margin-bottom: 24px; min-width: 0; }
|
|
271
293
|
.stitle { font-size: 14px; font-weight: 600; margin: 2px 0 12px; color: var(--text); }
|
|
272
294
|
.stitle.bar { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
273
|
-
.scard { background: #141820; border: 1px solid var(--line); border-radius: 10px; padding: 0 20px; }
|
|
295
|
+
.scard { background: #141820; border: 1px solid var(--line); border-radius: 10px; padding: 0 20px; min-width: 0; overflow: hidden; }
|
|
274
296
|
.scard.spadded { padding: 16px 20px; }
|
|
275
|
-
.srow { display: grid; grid-template-columns: 1fr auto; gap:
|
|
297
|
+
.srow { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 20px; align-items: center; padding: 15px 0; border-bottom: 1px solid var(--line); }
|
|
298
|
+
.srow.sblock { display: flex; flex-direction: column; align-items: stretch; gap: 12px; padding: 16px 0; }
|
|
276
299
|
.srow:last-child { border-bottom: 0; }
|
|
277
300
|
.srow[hidden] { display: none; }
|
|
278
301
|
.srow .slbl { font-weight: 600; font-size: 13.5px; color: var(--text); }
|
|
279
302
|
.srow .hint { font-size: 12.5px; color: var(--muted); margin-top: 3px; line-height: 1.45; }
|
|
280
303
|
.srow .mark { border: 0; padding: 0 0 0 8px; font-size: 11px; font-weight: 400; color: var(--yellow); text-decoration: underline dotted; }
|
|
281
|
-
.srow select.sctrl { background: transparent; border: 1px solid transparent; padding: 5px 8px; font-weight: 500; text-align: right; cursor: pointer; }
|
|
304
|
+
.srow select.sctrl { background: transparent; border: 1px solid transparent; padding: 5px 8px; font-weight: 500; text-align: right; cursor: pointer; max-width: 240px; }
|
|
282
305
|
.srow select.sctrl:hover, .srow select.sctrl:focus { background: var(--bg); border-color: var(--line); }
|
|
283
306
|
.srow input[type=number].sctrl { width: 110px; text-align: right; background: var(--bg); }
|
|
284
307
|
input[type=checkbox].switch { appearance: none; width: 38px; height: 22px; background: #2b313d; border: 1px solid #3a4252; border-radius: 999px; position: relative; cursor: pointer; transition: background .15s, border-color .15s; flex: none; padding: 0; }
|
|
@@ -300,11 +323,13 @@ input[type=checkbox].switch:checked::after { transform: translateX(16px); backgr
|
|
|
300
323
|
.llm-remote button { padding: 1px 8px; font-size: 12px; border-radius: 999px; color: var(--muted); }
|
|
301
324
|
.llm-remote button:disabled { opacity: .5; }
|
|
302
325
|
.row-gap { display: flex; gap: 8px; }
|
|
303
|
-
form#llm-add, form#broker-form { display: grid; gap: 10px; }
|
|
304
|
-
form#llm-add label, form#broker-form label { display: grid; grid-template-columns: 1fr 1.4fr; gap:
|
|
326
|
+
form#llm-add, form#broker-form { display: grid; gap: 10px; min-width: 0; }
|
|
327
|
+
form#llm-add label, form#broker-form label { display: grid; grid-template-columns: minmax(150px, 1fr) minmax(0, 1.4fr); gap: 12px; align-items: center; min-width: 0; }
|
|
328
|
+
form#llm-add label > input:not([type=checkbox]), form#llm-add label > select,
|
|
329
|
+
form#broker-form label > input:not([type=checkbox]), form#broker-form label > select { width: 100%; min-width: 0; max-width: 100%; text-overflow: ellipsis; }
|
|
305
330
|
form#llm-add label[hidden], form#broker-form label[hidden] { display: none; }
|
|
306
|
-
form#llm-add label.check, form#broker-form label.check { grid-template-columns: 1fr auto; }
|
|
307
|
-
form#llm-add input[type=checkbox] { width: 18px; height: 18px; }
|
|
331
|
+
form#llm-add label.check, form#broker-form label.check { grid-template-columns: minmax(0, 1fr) auto; }
|
|
332
|
+
form#llm-add input[type=checkbox]:not(.switch) { width: 18px; height: 18px; }
|
|
308
333
|
.flash { animation: flash 2.5s ease-out; }
|
|
309
334
|
@keyframes flash { 0% { background: var(--flash-bg); } 100% { background: transparent; } }
|
|
310
335
|
|
|
@@ -332,6 +357,15 @@ form#llm-add input[type=checkbox] { width: 18px; height: 18px; }
|
|
|
332
357
|
table.cmp { font-size: 12.5px; }
|
|
333
358
|
table.cmp th { padding: 4px 8px; font-size: 12px; }
|
|
334
359
|
table.cmp td { padding: 3px 8px; }
|
|
360
|
+
.secret-input { -webkit-text-security: disc; text-security: disc; font-family: monospace; }
|
|
361
|
+
.brk-subtabs { display: flex; gap: 6px; margin: 10px 0 8px; border-bottom: 1px solid var(--line); padding-bottom: 6px; }
|
|
362
|
+
.brk-subtabs button { background: transparent; border: 1px solid transparent; padding: 5px 12px; border-radius: 6px; font-weight: 500; color: var(--muted); }
|
|
363
|
+
.brk-subtabs button.active { background: var(--bg); border-color: var(--line); color: var(--text); font-weight: 600; }
|
|
364
|
+
.brk-alloc-wrap { margin: 6px 0 12px; padding: 8px 10px; background: var(--bg); border: 1px solid var(--line); border-radius: 8px; }
|
|
365
|
+
.brk-alloc-bar { display: flex; height: 10px; border-radius: 5px; overflow: hidden; gap: 2px; margin-bottom: 7px; background: var(--line); }
|
|
366
|
+
.brk-alloc-legend { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 12px; color: var(--muted); }
|
|
367
|
+
.brk-alloc-legend span { display: inline-flex; align-items: center; gap: 5px; }
|
|
368
|
+
.brk-alloc-legend i { width: 9px; height: 9px; border-radius: 2px; display: inline-block; }
|
|
335
369
|
|
|
336
370
|
@media (max-width: 900px) {
|
|
337
371
|
body { overflow: auto; }
|
package/server/api/routes.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roman A. Perepelkin. Licensed under the MIT License.
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import express from 'express';
|
|
4
5
|
import { ROOT, strategyDefaults } from '../config.js';
|
|
5
6
|
import { activeModelInfo } from '../llm/client.js';
|
|
6
|
-
import { listProviders, getSelected, addProvider, updateProvider, deleteProvider, fetchRemoteModels, testProvider, selectModel } from '../llm/providers.js';
|
|
7
|
-
import { listRuns, allRuns, getRun, getTrades, deleteRun, updateRunTitle, setRunShowcase, listShowcaseRuns, countUserRunsToday, countUserChatToday, listJournal, deleteJournal, addJournal, listSettingsHistory } from '../db.js';
|
|
8
|
-
import { PLANS, effectivePlan, publicUser, createSession, deleteSession, registerUser, loginUser, ensureNotifyCode, ensureApiKey, getUserByApiKey, activateLocalLicense, listAllUsers, setUserPlan, createPromoCode, listPromoCodes, redeemPromoCode, parseCookies } from '../auth/index.js';
|
|
7
|
+
import { listProviders, getSelected, addProvider, updateProvider, deleteProvider, fetchRemoteModels, testProvider, selectModel, resolveUpstreamForProxy, providerFetch } from '../llm/providers.js';
|
|
8
|
+
import { listRuns, allRuns, getRun, getTrades, deleteRun, updateRunTitle, setRunShowcase, listShowcaseRuns, countUserRunsToday, countUserChatToday, listJournal, deleteJournal, addJournal, listSettingsHistory, kvGet, kvSet } from '../db.js';
|
|
9
|
+
import { PLANS, effectivePlan, publicUser, getUserById, createSession, deleteSession, registerUser, loginUser, ensureNotifyCode, ensureApiKey, getUserByApiKey, activateLocalLicense, listAllUsers, setUserPlan, createPromoCode, listPromoCodes, redeemPromoCode, parseCookies, publicOAuthConfig, saveOAuthConfig, startOAuthRedirect, completeOAuthCallback, verifyTelegramLoginWidget } from '../auth/index.js';
|
|
9
10
|
import { publicBillingConfig, saveBillingConfig, createYookassaPayment, verifyYookassaPayment, verifyUserLatestPayment, listAllPayments } from '../billing/yookassa.js';
|
|
10
|
-
import { publicNotifyConfig, saveNotifyConfig, notifyUser } from '../notify/index.js';
|
|
11
|
+
import { publicNotifyConfig, getNotifyConfig, saveNotifyConfig, notifyUser, createTelegramLoginChallenge, checkTelegramLoginChallenge, handleMaxWebhook } from '../notify/index.js';
|
|
11
12
|
import { PARAMS } from '../params-meta.js';
|
|
12
13
|
import { getSettings, updateSettings, resetSettings, revertSettingsChange } from '../settings.js';
|
|
13
14
|
import { startModelResearch, cancelModelResearch, startStabilityCheck, cancelStabilityCheck } from '../model/research.js';
|
|
14
15
|
import { forecastNow } from '../model/forecast.js';
|
|
15
16
|
import { profilesInfo, startProfilesRefresh } from '../model/profiles.js';
|
|
16
17
|
import { live } from '../model/live.js';
|
|
17
|
-
import { brokersState, saveBrokerAccount, deleteBrokerAccount, syncBrokerAccount, prepareBrokerOrders, executeBrokerOrders, rejectBrokerOrders } from '../brokers/index.js';
|
|
18
|
+
import { brokersState, saveBrokerAccount, deleteBrokerAccount, syncBrokerAccount, discoverBrokerAccounts, freezeBrokerBaseline, fetchBrokerHistory, prepareBrokerOrders, executeBrokerOrders, rejectBrokerOrders } from '../brokers/index.js';
|
|
18
19
|
import { listNews } from '../news/index.js';
|
|
19
20
|
import * as strategist from '../strategist/index.js';
|
|
20
21
|
import { hub } from '../hub.js';
|
|
@@ -42,8 +43,62 @@ export function createRouter() {
|
|
|
42
43
|
cloudMode: process.env.CLOUD_MODE === '1',
|
|
43
44
|
plans: PLANS,
|
|
44
45
|
notify: publicNotifyConfig(),
|
|
46
|
+
oauth: publicOAuthConfig(),
|
|
45
47
|
billing: { enabled: publicBillingConfig().enabled },
|
|
46
48
|
}));
|
|
49
|
+
r.get('/auth/oauth/:provider/start', (req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
const origin = `${req.protocol}://${req.get('host')}`;
|
|
52
|
+
const url = startOAuthRedirect(req.params.provider, { origin, plan: req.query.plan || '' });
|
|
53
|
+
res.redirect(url);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
res.redirect(`/landing?oauth_error=${encodeURIComponent(e.message)}`);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
r.get('/auth/oauth/:provider/callback', async (req, res) => {
|
|
59
|
+
try {
|
|
60
|
+
const { user, plan } = await completeOAuthCallback(req.params.provider, req.query || {});
|
|
61
|
+
const token = createSession(user.id);
|
|
62
|
+
setCookie(res, token);
|
|
63
|
+
res.redirect(`/landing?oauth_ok=1${plan ? `&buy_plan=${encodeURIComponent(plan)}` : ''}`);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
res.redirect(`/landing?oauth_error=${encodeURIComponent(e.message)}`);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
r.get('/auth/telegram/callback', (req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
const { tgToken } = getNotifyConfig();
|
|
71
|
+
const user = verifyTelegramLoginWidget(req.query || {}, tgToken);
|
|
72
|
+
const token = createSession(user.id);
|
|
73
|
+
setCookie(res, token);
|
|
74
|
+
res.redirect('/landing?oauth_ok=1');
|
|
75
|
+
} catch (e) {
|
|
76
|
+
res.redirect(`/landing?oauth_error=${encodeURIComponent(e.message)}`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
// Быстрый вход в 1 клик через бота Telegram или MAX
|
|
80
|
+
r.post('/auth/telegram/start', wrap((req, res) => {
|
|
81
|
+
const ch = req.body?.channel === 'max' ? 'max' : 'tg';
|
|
82
|
+
const nt = publicNotifyConfig();
|
|
83
|
+
if (ch === 'max') {
|
|
84
|
+
if (!nt.hasMaxToken || !nt.maxBotName) throw new Error('Бот MAX ещё не подключён администратором');
|
|
85
|
+
const code = createTelegramLoginChallenge();
|
|
86
|
+
return res.json({ code, botUrl: `https://max.ru/${encodeURIComponent(nt.maxBotName)}?start=${encodeURIComponent(code)}` });
|
|
87
|
+
}
|
|
88
|
+
if (!nt.hasTgToken || !nt.tgBotName) throw new Error('Telegram-бот ещё не подключён администратором');
|
|
89
|
+
const code = createTelegramLoginChallenge();
|
|
90
|
+
res.json({ code, botUrl: `https://t.me/${encodeURIComponent(nt.tgBotName)}?start=${encodeURIComponent(code)}` });
|
|
91
|
+
}));
|
|
92
|
+
r.get('/auth/telegram/poll', (req, res) => {
|
|
93
|
+
const st = checkTelegramLoginChallenge(req.query.code);
|
|
94
|
+
if (st.status === 'done' && st.userId) {
|
|
95
|
+
const u = getUserById(st.userId);
|
|
96
|
+
const token = createSession(u.id);
|
|
97
|
+
setCookie(res, token);
|
|
98
|
+
return res.json({ status: 'done', user: publicUser(u), token });
|
|
99
|
+
}
|
|
100
|
+
res.json(st);
|
|
101
|
+
});
|
|
47
102
|
r.post('/auth/register', wrap((req, res) => {
|
|
48
103
|
const u = registerUser(req.body || {});
|
|
49
104
|
const token = createSession(u.id);
|
|
@@ -103,6 +158,7 @@ export function createRouter() {
|
|
|
103
158
|
// Активация API-ключа облачной подписки на локальном npx-клиенте
|
|
104
159
|
r.post('/license/activate', wrap(async (req, res) => {
|
|
105
160
|
const lic = await activateLocalLicense(req.body?.apiKey, req.body?.cloudUrl);
|
|
161
|
+
llmChanged();
|
|
106
162
|
res.json({ ok: true, license: lic, user: publicUser(req.user) });
|
|
107
163
|
}));
|
|
108
164
|
|
|
@@ -126,6 +182,9 @@ export function createRouter() {
|
|
|
126
182
|
if (pid) await verifyYookassaPayment(pid).catch(() => {});
|
|
127
183
|
res.json({ ok: true });
|
|
128
184
|
}));
|
|
185
|
+
r.post('/notify/max/webhook', wrap(async (req, res) => {
|
|
186
|
+
res.json(await handleMaxWebhook(req.body || {}));
|
|
187
|
+
}));
|
|
129
188
|
|
|
130
189
|
// ---------- публичная витрина для лендинга ----------
|
|
131
190
|
r.get('/public/showcase', (req, res) => {
|
|
@@ -144,31 +203,64 @@ export function createRouter() {
|
|
|
144
203
|
});
|
|
145
204
|
if (!runs.length) {
|
|
146
205
|
try {
|
|
147
|
-
const ref = JSON.parse(fs.readFileSync(path.join(ROOT, '
|
|
148
|
-
|
|
206
|
+
const ref = JSON.parse(fs.readFileSync(path.join(ROOT, 'config', 'seed-run.json'), 'utf8'));
|
|
207
|
+
const att = ref?.extra?.comparison?.attention;
|
|
208
|
+
if (att?.full) {
|
|
149
209
|
runs.push({
|
|
150
|
-
id:
|
|
151
|
-
title: 'Эталонная стратегия «Внимание через карту»',
|
|
152
|
-
params: { modelHorizon: 2, modelTopK: ref.params?.
|
|
153
|
-
metrics:
|
|
154
|
-
oos:
|
|
155
|
-
period: ref.period || {},
|
|
156
|
-
curve: (ref.curves?.attention || []).slice(-180),
|
|
157
|
-
marketCurve: (ref.curves?.market || []).slice(-180),
|
|
210
|
+
id: 1,
|
|
211
|
+
title: ref.params?._title || 'Эталонная адаптивная стратегия «Внимание через карту»',
|
|
212
|
+
params: { modelHorizon: ref.params?.modelHorizon || 2, modelTopK: ref.params?.modelTopK || 3, modelDirection: ref.params?.modelDirection || 'longshort', modelType: 'attention' },
|
|
213
|
+
metrics: att.full,
|
|
214
|
+
oos: att.outOfSample || {},
|
|
215
|
+
period: ref.metrics?.period || {},
|
|
216
|
+
curve: (ref.extra?.curves?.attention || []).slice(-180),
|
|
217
|
+
marketCurve: (ref.extra?.curves?.market || []).slice(-180),
|
|
158
218
|
});
|
|
159
219
|
}
|
|
160
220
|
} catch { /* ignore */ }
|
|
161
221
|
}
|
|
162
|
-
res.json({ runs, plans: PLANS });
|
|
222
|
+
res.json({ runs, plans: PLANS, legal: getLegalInfo() });
|
|
163
223
|
});
|
|
164
224
|
|
|
165
|
-
// ----------
|
|
225
|
+
// ---------- реквизиты оферты (для модерации ЮKassa) ----------
|
|
226
|
+
const DEFAULT_LEGAL = {
|
|
227
|
+
ownerName: 'Перепелкин Роман Александрович',
|
|
228
|
+
status: 'Индивидуальный предприниматель / Плательщик НПД',
|
|
229
|
+
inn: '',
|
|
230
|
+
ogrn: '',
|
|
231
|
+
email: 'support@neuroalpha.ru',
|
|
232
|
+
phone: '',
|
|
233
|
+
address: 'Российская Федерация',
|
|
234
|
+
};
|
|
235
|
+
const getLegalInfo = () => ({ ...DEFAULT_LEGAL, ...(kvGet('legalInfo') || {}) });
|
|
236
|
+
r.get('/public/legal', (req, res) => res.json(getLegalInfo()));
|
|
237
|
+
|
|
238
|
+
// ---------- админ-панель (пользователи, оплаты, ЮKassa, боты Telegram и MAX, витрина, реквизиты) ----------
|
|
166
239
|
const requireAdmin = (req) => {
|
|
167
240
|
if (!req.user || req.user.role !== 'admin') throw new Error('Доступно только администратору платформы');
|
|
168
241
|
};
|
|
169
242
|
r.get('/admin/state', wrap((req, res) => {
|
|
170
243
|
requireAdmin(req);
|
|
171
|
-
res.json({ users: listAllUsers(), payments: listAllPayments(), promos: listPromoCodes(), billing: publicBillingConfig(), notify: publicNotifyConfig() });
|
|
244
|
+
res.json({ users: listAllUsers(), payments: listAllPayments(), promos: listPromoCodes(), billing: publicBillingConfig(), notify: publicNotifyConfig(), oauth: publicOAuthConfig(), legal: getLegalInfo() });
|
|
245
|
+
}));
|
|
246
|
+
r.post('/admin/oauth', wrap((req, res) => {
|
|
247
|
+
requireAdmin(req);
|
|
248
|
+
res.json({ ok: true, oauth: saveOAuthConfig(req.body || {}) });
|
|
249
|
+
}));
|
|
250
|
+
r.post('/admin/legal', wrap((req, res) => {
|
|
251
|
+
requireAdmin(req);
|
|
252
|
+
const b = req.body || {};
|
|
253
|
+
const next = {
|
|
254
|
+
ownerName: String(b.ownerName ?? '').trim(),
|
|
255
|
+
status: String(b.status ?? '').trim(),
|
|
256
|
+
inn: String(b.inn ?? '').trim(),
|
|
257
|
+
ogrn: String(b.ogrn ?? '').trim(),
|
|
258
|
+
email: String(b.email ?? '').trim(),
|
|
259
|
+
phone: String(b.phone ?? '').trim(),
|
|
260
|
+
address: String(b.address ?? '').trim(),
|
|
261
|
+
};
|
|
262
|
+
kvSet('legalInfo', next);
|
|
263
|
+
res.json({ ok: true, legal: getLegalInfo() });
|
|
172
264
|
}));
|
|
173
265
|
r.post('/admin/promo', wrap((req, res) => {
|
|
174
266
|
requireAdmin(req);
|
|
@@ -182,9 +274,9 @@ export function createRouter() {
|
|
|
182
274
|
requireAdmin(req);
|
|
183
275
|
res.json(saveBillingConfig(req.body || {}));
|
|
184
276
|
}));
|
|
185
|
-
r.post('/admin/notify', wrap((req, res) => {
|
|
277
|
+
r.post('/admin/notify', wrap(async (req, res) => {
|
|
186
278
|
requireAdmin(req);
|
|
187
|
-
res.json(saveNotifyConfig(req.body || {}));
|
|
279
|
+
res.json(await saveNotifyConfig(req.body || {}));
|
|
188
280
|
}));
|
|
189
281
|
r.put('/runs/:id/showcase', wrap((req, res) => {
|
|
190
282
|
requireAdmin(req);
|
|
@@ -205,6 +297,67 @@ export function createRouter() {
|
|
|
205
297
|
r.post('/llm/test', wrap(async (req, res) => res.json(await testProvider(req.body?.providerId, req.body?.model))));
|
|
206
298
|
r.post('/llm/select', wrap((req, res) => { selectModel(req.body?.providerId, req.body?.model); llmChanged(); res.json(llmState()); }));
|
|
207
299
|
|
|
300
|
+
// ---------- облачный ИИ-прокси для локальных npx-клиентов по APIKey или в демо-режиме ----------
|
|
301
|
+
const proxyUsesToday = (ident) => {
|
|
302
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
303
|
+
const key = `llmProxy:${ident}:${day}`;
|
|
304
|
+
return { key, count: Number(kvGet(key, 0)) || 0 };
|
|
305
|
+
};
|
|
306
|
+
r.get('/llm/proxy/v1/models', (req, res) => {
|
|
307
|
+
res.json({ data: [{ id: 'Qwen/Qwen3.8-27B-FP8', object: 'model' }] });
|
|
308
|
+
});
|
|
309
|
+
r.post('/llm/proxy/v1/chat/completions', async (req, res) => {
|
|
310
|
+
try {
|
|
311
|
+
const ip = String(req.headers['x-forwarded-for'] || req.socket?.remoteAddress || 'anon').split(',')[0].trim();
|
|
312
|
+
let maxPerDay = 5;
|
|
313
|
+
let ident = `ip:${ip}`;
|
|
314
|
+
let planLabel = 'Демо (без ключа)';
|
|
315
|
+
if (req.user) {
|
|
316
|
+
const plan = effectivePlan(req.user);
|
|
317
|
+
maxPerDay = plan.maxChatPerDay;
|
|
318
|
+
ident = `u:${req.user.id}`;
|
|
319
|
+
planLabel = plan.name;
|
|
320
|
+
}
|
|
321
|
+
const u = proxyUsesToday(ident);
|
|
322
|
+
if (u.count >= maxPerDay) {
|
|
323
|
+
return res.status(429).json({
|
|
324
|
+
error: {
|
|
325
|
+
message: req.user
|
|
326
|
+
? `Исчерпан дневной лимит ИИ-запросов по тарифу «${planLabel}» (${maxPerDay} в сутки). Расширьте тариф на https://neuroalpha.odant.org`
|
|
327
|
+
: `Исчерпан бесплатный демо-лимит (${maxPerDay} запросов в сутки). Получите бесплатный или платный APIKey на https://neuroalpha.odant.org`,
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
const t = resolveUpstreamForProxy();
|
|
332
|
+
if (!t) {
|
|
333
|
+
return res.status(503).json({ error: { message: 'На сервере neuroalpha.odant.org ещё не подключена апстрим-модель ИИ' } });
|
|
334
|
+
}
|
|
335
|
+
kvSet(u.key, u.count + 1);
|
|
336
|
+
const body = { ...(req.body || {}), model: t.model, stream: true };
|
|
337
|
+
if (!body.reasoning_effort && t.effort) body.reasoning_effort = t.effort;
|
|
338
|
+
const upstream = await providerFetch(t.provider, t.url, {
|
|
339
|
+
method: 'POST',
|
|
340
|
+
headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' },
|
|
341
|
+
body: JSON.stringify(body),
|
|
342
|
+
signal: AbortSignal.timeout(90000),
|
|
343
|
+
});
|
|
344
|
+
if (!upstream.ok) {
|
|
345
|
+
const txt = await upstream.text().catch(() => '');
|
|
346
|
+
return res.status(upstream.status).json({ error: { message: txt.slice(0, 300) || `HTTP ${upstream.status}` } });
|
|
347
|
+
}
|
|
348
|
+
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
|
349
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
350
|
+
res.setHeader('Connection', 'keep-alive');
|
|
351
|
+
for await (const chunk of upstream.body) {
|
|
352
|
+
res.write(chunk);
|
|
353
|
+
}
|
|
354
|
+
res.end();
|
|
355
|
+
} catch (e) {
|
|
356
|
+
if (!res.headersSent) res.status(500).json({ error: { message: e.message } });
|
|
357
|
+
else res.end();
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
208
361
|
// ---------- настройки ----------
|
|
209
362
|
r.get('/settings', (req, res) => res.json(getSettings()));
|
|
210
363
|
r.put('/settings', wrap((req, res) => res.json(updateSettings(req.body || {}, { source: 'user' }))));
|
|
@@ -256,9 +409,6 @@ export function createRouter() {
|
|
|
256
409
|
}
|
|
257
410
|
const body = req.body || {};
|
|
258
411
|
const merged = { ...getSettings(), ...body };
|
|
259
|
-
if ((merged.modelUseTransformer || merged.modelType === 'transformer') && !plan.allowTransformer) {
|
|
260
|
-
throw new Error('Обучение нейросети-трансформера доступно на тарифе «Профи / Лаборатория».');
|
|
261
|
-
}
|
|
262
412
|
const { runId, reused } = startModelResearch(merged, { source: 'user', force: !!body.force });
|
|
263
413
|
res.json({ runId, reused: !!reused });
|
|
264
414
|
}));
|
|
@@ -282,6 +432,7 @@ export function createRouter() {
|
|
|
282
432
|
|
|
283
433
|
// ---------- брокерские счета (Т-Банк, Финам, Алор, БКС) ----------
|
|
284
434
|
r.get('/brokers', (req, res) => res.json(brokersState()));
|
|
435
|
+
r.post('/brokers/discover', wrap(async (req, res) => res.json(await discoverBrokerAccounts(req.body || {}))));
|
|
285
436
|
r.post('/brokers', wrap((req, res) => {
|
|
286
437
|
const plan = effectivePlan(req.user);
|
|
287
438
|
const cur = brokersState().accounts.length;
|
|
@@ -293,6 +444,8 @@ export function createRouter() {
|
|
|
293
444
|
r.put('/brokers/:id', wrap((req, res) => res.json(saveBrokerAccount({ ...(req.body || {}), id: req.params.id }))));
|
|
294
445
|
r.delete('/brokers/:id', wrap((req, res) => res.json(deleteBrokerAccount(req.params.id))));
|
|
295
446
|
r.post('/brokers/:id/sync', wrap(async (req, res) => res.json(await syncBrokerAccount(req.params.id))));
|
|
447
|
+
r.post('/brokers/:id/history', wrap(async (req, res) => res.json(await fetchBrokerHistory(req.params.id))));
|
|
448
|
+
r.post('/brokers/:id/freeze-baseline', wrap(async (req, res) => res.json(await freezeBrokerBaseline(req.params.id))));
|
|
296
449
|
r.post('/brokers/:id/prepare', wrap(async (req, res) => res.json(await prepareBrokerOrders(req.params.id))));
|
|
297
450
|
r.post('/brokers/:id/execute', wrap(async (req, res) => res.json(await executeBrokerOrders(req.params.id))));
|
|
298
451
|
r.post('/brokers/:id/reject', wrap((req, res) => res.json(rejectBrokerOrders(req.params.id))));
|