neuroalpha 0.2.0 → 0.3.13
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 +17 -0
- package/README.md +9 -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 +449 -0
- package/public/app.js +1182 -379
- package/public/index.html +87 -22
- package/public/landing.html +1217 -870
- package/public/legal.html +51 -8
- package/public/styles.css +71 -16
- package/server/api/routes.js +220 -35
- package/server/auth/index.js +633 -340
- package/server/billing/yookassa.js +1 -0
- package/server/brokers/index.js +1369 -645
- package/server/config.js +6 -4
- package/server/data/moex.js +1 -0
- package/server/db.js +69 -4
- 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 +202 -37
- package/server/model/linalg.js +1 -0
- package/server/model/live.js +476 -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 +128 -205
- 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 +22 -26
- package/server/settings.js +1 -0
- package/server/strategist/analytics.js +2 -1
- package/server/strategist/index.js +7 -7
- 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. All rights reserved. Proprietary software. -->
|
|
1
2
|
<!doctype html>
|
|
2
3
|
<html lang="ru">
|
|
3
4
|
<head>
|
|
@@ -40,22 +41,37 @@
|
|
|
40
41
|
|
|
41
42
|
<h3>1.1. Предмет договора и правовой статус сервиса</h3>
|
|
42
43
|
<ul>
|
|
43
|
-
<li>Исполнитель предоставляет Пользователю дистанционный доступ к функциям
|
|
44
|
-
<li>Сервис «НейроАльфа» является исключительно программно-техническим средством (калькулятором статистических закономерностей и интерфейсом автоматизации отправки поручений по
|
|
45
|
-
<li><b>Исполнитель не осуществляет деятельность по инвестиционному консультированию</b> (в смысле ст. 6.1 Федерального закона № 39-ФЗ «О рынке ценных бумаг»), не составляет индивидуальных инвестиционных профилей и не даёт индивидуальных инвестиционных
|
|
44
|
+
<li>Исполнитель предоставляет Пользователю дистанционный доступ к функциям облачного программно-вычислительного комплекса «НейроАльфа» (модель PaaS/SaaS) в соответствии с выбранным Пользователем тарифным планом («Старт», «Инвестор», «Профи», «VIP»).</li>
|
|
45
|
+
<li>Сервис «НейроАльфа» является исключительно программно-техническим средством (калькулятором статистических закономерностей и интерфейсом автоматизации отправки поручений по параметрам и предохранителям, заданным самим Пользователем).</li>
|
|
46
|
+
<li><b>Исполнитель не осуществляет деятельность по инвестиционному консультированию</b> (в смысле ст. 6.1 Федерального закона № 39-ФЗ «О рынке ценных бумаг»), не составляет индивидуальных инвестиционных профилей и не даёт индивидуальных инвестиционных рекомендаций (ИИР).</li>
|
|
46
47
|
<li><b>Исполнитель не осуществляет доверительное управление</b> или брокерскую деятельность, не принимает денежные средства или ценные бумаги Пользователей на свои счета. Все торговые операции совершаются Пользователем самостоятельно на его личном счёте у лицензированного брокера.</li>
|
|
47
48
|
</ul>
|
|
48
49
|
|
|
49
|
-
<h3>1.2.
|
|
50
|
+
<h3>1.2. Тарифные планы и технические параметры мощности</h3>
|
|
50
51
|
<ul>
|
|
51
|
-
<li
|
|
52
|
+
<li><b>Тариф «Старт (Тест-драйв)» (0 ₽):</b> предоставляет доступ к историческим исследованиям (экспериментам) и 1 виртуальному демо-счёту без возможности подключения реальных брокерских счетов.</li>
|
|
53
|
+
<li><b>Тариф «Инвестор»:</b> позволяет подключить до 2 реальных брокерских счетов с ограничением выделенного капитала до 1 000 000 ₽ на каждом счёте, работающих исключительно в режиме покупок («Только в рост (без шорта)») с консервативным профилем загрузки слота и предельным целевым темпом доходности стратегии до 50% годовых.</li>
|
|
54
|
+
<li><b>Тариф «Профи»:</b> позволяет подключить до 5 реальных брокерских счетов с ограничением выделенного капитала до 10 000 000 ₽ на каждом счёте, включает режим работы на росте и падении акций («В рост и в падение (с шортом)») со стандартным шагом ребалансировки и предельным целевым темпом доходности стратегии до 100% годовых.</li>
|
|
55
|
+
<li><b>Тариф «VIP (Без ограничений)»:</b> предоставляется по индивидуальному согласованию с Администрацией платформы (через персональный VIP-токен), снимает ограничения на количество счетов, объём торгового капитала и потолок годовой доходности, активируя полный режим мощности ядра (внутричасовое полушаговое сопровождение <code>H/2</code> и динамическую концентрацию слота <code>Conviction Sizing</code>).</li>
|
|
56
|
+
</ul>
|
|
57
|
+
|
|
58
|
+
<h3>1.3. Запрет использования автоследования и ретрансляции сигналов</h3>
|
|
59
|
+
<ul>
|
|
60
|
+
<li>Пользователю <b>строго запрещается</b> подключать к платформе «НейроАльфа» брокерские счета (субсчета), участвующие в любых сервисах автоследования или копирования сделок (как в роли автора/ведущего стратегии, так и в роли подписчика), включая, но не ограничиваясь: «Сигналы» Т-Инвестиций, Finam Comon, Автоследование БКС и сторонние копи-трейдинг сервисы.</li>
|
|
61
|
+
<li>Пользователю запрещается передавать, публиковать, ретранслировать или перепродавать третьим лицам формируемые платформой расчёты, прогнозы и пакеты торговых поручений.</li>
|
|
62
|
+
<li>Платформа оснащена автоматическим техническим контролем признаков автоследования. При обнаружении на подключённом субсчёте операций автоследования или ретрансляции сделок подача поручений по счёту автоматически блокируется, а действие подписки может быть прекращено Исполнителем в одностороннем порядке без возврата оплаченных средств.</li>
|
|
63
|
+
</ul>
|
|
64
|
+
|
|
65
|
+
<h3>1.4. Порядок оплаты и предоставления доступа</h3>
|
|
66
|
+
<ul>
|
|
67
|
+
<li>Оплата тарифных планов производится в рублях РФ безналичным способом (банковские карты МИР, Visa, Mastercard, Система быстрых платежей) либо путём активации промокода / персонального VIP-токена.</li>
|
|
52
68
|
<li>Доступ к оплаченному тарифному плану активируется автоматически в момент получения подтверждения об успешной оплате (`succeeded`) от платёжной системы на срок оплаченного периода (30, 90 или 365 календарных дней).</li>
|
|
53
|
-
<li>Услуга предоставления доступа к SaaS-платформе считается оказанной надлежащим образом с момента открытия технической возможности использования
|
|
69
|
+
<li>Услуга предоставления доступа к PaaS/SaaS-платформе считается оказанной надлежащим образом с момента открытия технической возможности использования выбранного тарифа в учётной записи Пользователя.</li>
|
|
54
70
|
</ul>
|
|
55
71
|
|
|
56
|
-
<h3>1.
|
|
72
|
+
<h3>1.5. Ограничение ответственности</h3>
|
|
57
73
|
<ul>
|
|
58
|
-
<li>Программное обеспечение предоставляется по принципу «как есть» (as is). Исполнитель не гарантирует достижения какой-либо положительной доходности при использовании аналитических расчётов
|
|
74
|
+
<li>Программное обеспечение предоставляется по принципу «как есть» (as is). Исполнитель не гарантирует достижения какой-либо положительной доходности при использовании аналитических расчётов программы. Указанные в описании тарифов предельные уровни годовой доходности (50% / 100% годовых) являются верхними техническими порогами алгоритма управления риском, а не обещанием или гарантией дохода.</li>
|
|
59
75
|
<li>Пользователь самостоятельно задаёт параметры стратегии, лимиты суммы, пороги дневного стопа и несёт полную имущественную ответственность за результаты сделок, совершаемых через подключённые им API-токены брокеров.</li>
|
|
60
76
|
</ul>
|
|
61
77
|
</section>
|
|
@@ -85,6 +101,33 @@
|
|
|
85
101
|
<li>Используя сервис «НейроАльфа», Пользователь подтверждает, что осознаёт указанные риски и принимает все инвестиционные решения самостоятельно.</li>
|
|
86
102
|
</ul>
|
|
87
103
|
</section>
|
|
104
|
+
|
|
105
|
+
<section class="doc" id="requisites">
|
|
106
|
+
<h2>4. Правообладатель, Оператор платформы и контакты поддержки</h2>
|
|
107
|
+
<div id="legal-req-body">
|
|
108
|
+
<ul>
|
|
109
|
+
<li><b>Оператор платформы (Исполнитель):</b> ООО «БИС» («Бизнес Интер Софт», г. Рязань)</li>
|
|
110
|
+
<li><b>Автор и правообладатель ПО «НейроАльфа»:</b> Перепелкин Роман Александрович</li>
|
|
111
|
+
<li><b>Лицензионный статус:</b> Проприетарное программное обеспечение (Все права защищены). Копирование, декомпиляция, модификация и распространение без письменного согласия Правообладателя запрещены.</li>
|
|
112
|
+
</ul>
|
|
113
|
+
</div>
|
|
114
|
+
</section>
|
|
115
|
+
<div style="text-align:center;color:var(--muted);font-size:13px;margin-top:16px">© 2026 Автор: Перепелкин Р.А. · Оператор: ООО «БИС» («Бизнес Интер Софт», г. Рязань) · Все права защищены</div>
|
|
88
116
|
</div>
|
|
117
|
+
<script>
|
|
118
|
+
fetch('/api/public/legal').then((r) => r.json()).then((d) => {
|
|
119
|
+
const rows = [
|
|
120
|
+
`<li><b>Оператор платформы (Исполнитель):</b> ${d.ownerName || 'ООО «БИС» («Бизнес Интер Софт»)'}${d.status ? ` (${d.status})` : ''}</li>`,
|
|
121
|
+
`<li><b>Автор и правообладатель ПО:</b> ${d.authorName || 'Перепелкин Роман Александрович'}</li>`,
|
|
122
|
+
d.inn ? `<li><b>ИНН:</b> ${d.inn}</li>` : '',
|
|
123
|
+
d.ogrn ? `<li><b>ОГРН:</b> ${d.ogrn}</li>` : '',
|
|
124
|
+
d.email ? `<li><b>Email технической поддержки:</b> <a href="mailto:${d.email}">${d.email}</a></li>` : '',
|
|
125
|
+
d.phone ? `<li><b>Телефон:</b> ${d.phone}</li>` : '',
|
|
126
|
+
`<li><b>Местонахождение:</b> ${d.address || 'Российская Федерация, г. Рязань'}</li>`,
|
|
127
|
+
'<li><b>Правовой режим ПО:</b> Проприетарное программное обеспечение (Все права защищены)</li>',
|
|
128
|
+
].filter(Boolean).join('');
|
|
129
|
+
document.getElementById('legal-req-body').innerHTML = `<ul>${rows}</ul>`;
|
|
130
|
+
}).catch(() => {});
|
|
131
|
+
</script>
|
|
89
132
|
</body>
|
|
90
133
|
</html>
|
package/public/styles.css
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software. */
|
|
1
2
|
:root {
|
|
2
3
|
--bg: #0d1016;
|
|
3
4
|
--panel: #13171f;
|
|
@@ -56,6 +57,9 @@ button.icon { padding: 2px 8px; color: var(--text); background: transparent; }
|
|
|
56
57
|
button.icon-lg { width: 40px; height: 40px; padding: 0; font-size: 15px; flex: none; }
|
|
57
58
|
|
|
58
59
|
main { display: grid; grid-template-columns: var(--chat-w) 5px 1fr; height: calc(100vh - 46px); }
|
|
60
|
+
body.chat-closed main { grid-template-columns: 1fr; }
|
|
61
|
+
body.chat-closed .chat, body.chat-closed #splitter { display: none; }
|
|
62
|
+
#btn-chat-toggle.active { border-color: var(--accent); color: var(--accent); background: rgba(79, 140, 255, 0.1); }
|
|
59
63
|
#splitter { cursor: col-resize; background: var(--line); transition: background .15s; }
|
|
60
64
|
#splitter:hover, #splitter.drag { background: var(--accent); }
|
|
61
65
|
body.dragging { cursor: col-resize; user-select: none; }
|
|
@@ -147,7 +151,11 @@ body.light .model-opt:hover:not(:disabled), body.light .model-opt.active { backg
|
|
|
147
151
|
.model-manage-btn:hover { background: #ffffff0c; color: var(--accent); border-color: var(--line); }
|
|
148
152
|
body.light .model-manage-btn:hover { background: #f0f3f6; }
|
|
149
153
|
.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]
|
|
154
|
+
.pbtn[hidden] { display: none; }
|
|
155
|
+
#chat-action svg { display: none; }
|
|
156
|
+
#chat-action[data-icon="mic"] #ico-mic { display: block; }
|
|
157
|
+
#chat-action[data-icon="send"] #ico-send { display: block; }
|
|
158
|
+
#chat-action[data-icon="stop"] #ico-stop { display: block; }
|
|
151
159
|
.pbtn.action-btn { background: #ffffff0c; border-color: transparent; color: var(--text); }
|
|
152
160
|
body.light .pbtn.action-btn { background: #eef1f5; color: #374151; }
|
|
153
161
|
.pbtn.action-btn:hover:not(:disabled) { border-color: var(--violet); color: var(--violet); }
|
|
@@ -182,12 +190,28 @@ body.light .tbadge.run { background: #dbeafe; border-color: #93c5fd; }
|
|
|
182
190
|
.progress .bar { position: absolute; inset: 0 auto 0 0; width: 0; background: #24467f; transition: width .2s; }
|
|
183
191
|
.progress span { position: relative; font-size: 12px; padding: 0 8px; line-height: 22px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; }
|
|
184
192
|
.empty { margin-top: 30px; color: var(--muted); line-height: 1.7; max-width: 640px; }
|
|
193
|
+
.empty.live-dash { margin-top: 14px; max-width: none; color: var(--text); line-height: 1.45; }
|
|
185
194
|
.empty b { color: var(--text); }
|
|
186
|
-
.
|
|
187
|
-
|
|
195
|
+
.ld-grid { display: grid; grid-template-columns: 1.15fr 0.85fr; gap: 16px; margin-top: 12px; }
|
|
196
|
+
@media (max-width: 1100px) { .ld-grid { grid-template-columns: 1fr; } }
|
|
197
|
+
.ld-card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; min-width: 0; }
|
|
198
|
+
.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; }
|
|
199
|
+
.ld-tl { position: relative; height: 26px; background: var(--bg); border: 1px solid var(--line); border-radius: 7px; overflow: hidden; margin: 8px 0 6px; }
|
|
200
|
+
.ld-tl-done { position: absolute; top: 0; bottom: 0; background: rgba(63, 185, 80, 0.16); transition: left .25s, width .25s; }
|
|
201
|
+
.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; }
|
|
202
|
+
.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; }
|
|
203
|
+
.ld-ics { display: flex; align-items: flex-end; gap: 3px; height: 64px; padding-top: 8px; border-bottom: 1px solid var(--line); margin-top: 8px; }
|
|
204
|
+
.ld-ic-bar { flex: 1; min-width: 4px; max-width: 18px; border-radius: 2px 2px 0 0; transition: height .2s; }
|
|
205
|
+
.ld-syms { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 10px; }
|
|
206
|
+
.ld-sym { font-size: 11px; padding: 2px 7px; border-radius: 5px; background: var(--bg); border: 1px solid var(--line); color: var(--text); }
|
|
207
|
+
.ld-sym.ok { border-color: rgba(63, 185, 80, 0.5); color: var(--green); }
|
|
208
|
+
.verdict { margin: 10px 0; font-size: 13.5px; line-height: 1.45; }
|
|
209
|
+
.kpis { display: grid; grid-template-columns: repeat(5, 1fr); gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
|
|
210
|
+
@media (max-width: 1180px) { .kpis { grid-template-columns: repeat(3, 1fr); } }
|
|
188
211
|
.kpi { background: var(--bg); padding: 10px 14px; cursor: help; }
|
|
189
|
-
.kpi .v { font-size:
|
|
190
|
-
.kpi .l { font-size: 12px; color: var(--muted); }
|
|
212
|
+
.kpi .v { font-size: 21px; font-weight: 600; line-height: 1.2; }
|
|
213
|
+
.kpi .l { font-size: 12px; color: var(--muted); margin-top: 2px; }
|
|
214
|
+
.kpi .sub { font-size: 11.5px; color: var(--muted); margin-top: 2px; line-height: 1.3; }
|
|
191
215
|
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: 6px; overflow: hidden; margin: 14px 0 6px; }
|
|
192
216
|
.seg button { border: 0; border-radius: 0; padding: 3px 10px; font-size: 12px; color: var(--muted); }
|
|
193
217
|
.seg button.active { background: var(--panel); color: var(--text); }
|
|
@@ -232,6 +256,11 @@ tr.best td { background: var(--best-bg); }
|
|
|
232
256
|
.stab-chip { background: var(--bg); border: 1px solid var(--line); border-radius: 6px; padding: 2px 8px; font-size: 12px; }
|
|
233
257
|
.live-card { background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; margin-top: 10px; }
|
|
234
258
|
.live-card.stopped { opacity: .75; }
|
|
259
|
+
.live-card.collapsed { padding: 10px 14px; }
|
|
260
|
+
.acc-fold-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; cursor: pointer; user-select: none; }
|
|
261
|
+
.acc-fold-head:hover b { color: var(--accent); }
|
|
262
|
+
.acc-caret { display: inline-block; width: 16px; color: var(--muted); font-size: 12px; transition: transform .15s; }
|
|
263
|
+
.live-card:not(.collapsed) .acc-caret { transform: rotate(90deg); color: var(--accent); }
|
|
235
264
|
.live-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 10px; }
|
|
236
265
|
.stab-box[hidden], .run-bar[hidden], .run-bar button[hidden] { display: none; }
|
|
237
266
|
.run-bar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; margin: 8px 0 4px; padding-bottom: 10px; border-bottom: 1px solid var(--line); }
|
|
@@ -258,27 +287,29 @@ body.light input[type=checkbox].switch:checked { background: #1a7f37; border-col
|
|
|
258
287
|
.headbar .b { height: 6px; background: var(--panel); border-radius: 3px; position: relative; }
|
|
259
288
|
.headbar .b div { position: absolute; top: 0; bottom: 0; border-radius: 3px; }
|
|
260
289
|
|
|
261
|
-
.settings-wrap { display: grid; grid-template-columns:
|
|
262
|
-
|
|
290
|
+
.settings-wrap { display: grid; grid-template-columns: 205px minmax(0, 1fr); gap: 24px; align-items: start; min-width: 0; }
|
|
291
|
+
@media (max-width: 1100px) { .settings-wrap { grid-template-columns: 180px minmax(0, 1fr); gap: 16px; } }
|
|
292
|
+
.snav { display: flex; flex-direction: column; gap: 3px; position: sticky; top: 0; min-width: 0; }
|
|
263
293
|
.snav #s-search { width: 100%; margin-bottom: 10px; background: transparent; border-color: var(--line); padding: 6px 10px; font-size: 13px; }
|
|
264
294
|
.snav button { text-align: left; border: 0; padding: 7px 12px; border-radius: 6px; color: var(--muted); font-size: 13.5px; }
|
|
265
295
|
.snav button:hover { background: #ffffff08; color: var(--text); }
|
|
266
296
|
.snav button.active { background: #262b36; color: var(--text); font-weight: 500; }
|
|
267
297
|
.snav .ssep { height: 1px; background: var(--line); margin: 8px 4px; }
|
|
268
298
|
.snav .sreset { color: var(--muted); font-size: 12.5px; }
|
|
269
|
-
.spane { min-width: 0; }
|
|
270
|
-
.ssec { margin-bottom: 24px; }
|
|
299
|
+
.spane { min-width: 0; overflow: hidden; }
|
|
300
|
+
.ssec { margin-bottom: 24px; min-width: 0; }
|
|
271
301
|
.stitle { font-size: 14px; font-weight: 600; margin: 2px 0 12px; color: var(--text); }
|
|
272
302
|
.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; }
|
|
303
|
+
.scard { background: #141820; border: 1px solid var(--line); border-radius: 10px; padding: 0 20px; min-width: 0; overflow: hidden; }
|
|
274
304
|
.scard.spadded { padding: 16px 20px; }
|
|
275
|
-
.srow { display: grid; grid-template-columns: 1fr auto; gap:
|
|
305
|
+
.srow { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 20px; align-items: center; padding: 15px 0; border-bottom: 1px solid var(--line); }
|
|
306
|
+
.srow.sblock { display: flex; flex-direction: column; align-items: stretch; gap: 12px; padding: 16px 0; }
|
|
276
307
|
.srow:last-child { border-bottom: 0; }
|
|
277
308
|
.srow[hidden] { display: none; }
|
|
278
309
|
.srow .slbl { font-weight: 600; font-size: 13.5px; color: var(--text); }
|
|
279
310
|
.srow .hint { font-size: 12.5px; color: var(--muted); margin-top: 3px; line-height: 1.45; }
|
|
280
311
|
.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; }
|
|
312
|
+
.srow select.sctrl { background: transparent; border: 1px solid transparent; padding: 5px 8px; font-weight: 500; text-align: right; cursor: pointer; max-width: 240px; }
|
|
282
313
|
.srow select.sctrl:hover, .srow select.sctrl:focus { background: var(--bg); border-color: var(--line); }
|
|
283
314
|
.srow input[type=number].sctrl { width: 110px; text-align: right; background: var(--bg); }
|
|
284
315
|
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 +331,13 @@ input[type=checkbox].switch:checked::after { transform: translateX(16px); backgr
|
|
|
300
331
|
.llm-remote button { padding: 1px 8px; font-size: 12px; border-radius: 999px; color: var(--muted); }
|
|
301
332
|
.llm-remote button:disabled { opacity: .5; }
|
|
302
333
|
.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:
|
|
334
|
+
form#llm-add, form#broker-form { display: grid; gap: 10px; min-width: 0; }
|
|
335
|
+
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; }
|
|
336
|
+
form#llm-add label > input:not([type=checkbox]), form#llm-add label > select,
|
|
337
|
+
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
338
|
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; }
|
|
339
|
+
form#llm-add label.check, form#broker-form label.check { grid-template-columns: minmax(0, 1fr) auto; }
|
|
340
|
+
form#llm-add input[type=checkbox]:not(.switch) { width: 18px; height: 18px; }
|
|
308
341
|
.flash { animation: flash 2.5s ease-out; }
|
|
309
342
|
@keyframes flash { 0% { background: var(--flash-bg); } 100% { background: transparent; } }
|
|
310
343
|
|
|
@@ -332,6 +365,28 @@ form#llm-add input[type=checkbox] { width: 18px; height: 18px; }
|
|
|
332
365
|
table.cmp { font-size: 12.5px; }
|
|
333
366
|
table.cmp th { padding: 4px 8px; font-size: 12px; }
|
|
334
367
|
table.cmp td { padding: 3px 8px; }
|
|
368
|
+
.secret-input { -webkit-text-security: disc; text-security: disc; font-family: monospace; }
|
|
369
|
+
.brk-subtabs { display: flex; gap: 6px; margin: 10px 0 8px; border-bottom: 1px solid var(--line); padding-bottom: 6px; }
|
|
370
|
+
.brk-subtabs button { background: transparent; border: 1px solid transparent; padding: 5px 12px; border-radius: 6px; font-weight: 500; color: var(--muted); }
|
|
371
|
+
.brk-subtabs button.active { background: var(--bg); border-color: var(--line); color: var(--text); font-weight: 600; }
|
|
372
|
+
.brk-alloc-wrap { margin: 6px 0 12px; padding: 8px 10px; background: var(--bg); border: 1px solid var(--line); border-radius: 8px; }
|
|
373
|
+
.brk-alloc-bar { display: flex; height: 10px; border-radius: 5px; overflow: hidden; gap: 2px; margin-bottom: 7px; background: var(--line); }
|
|
374
|
+
.brk-alloc-legend { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 12px; color: var(--muted); }
|
|
375
|
+
.brk-alloc-legend span { display: inline-flex; align-items: center; gap: 5px; }
|
|
376
|
+
.brk-alloc-legend i { width: 9px; height: 9px; border-radius: 2px; display: inline-block; }
|
|
377
|
+
.brk-specs { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 4px 14px; font-size: 13px; margin: 10px 0; line-height: 1.45; }
|
|
378
|
+
.brk-specs .k { color: var(--muted); }
|
|
379
|
+
.brk-specs .v { color: var(--text); }
|
|
380
|
+
.brk-toolbar { display: flex; flex-wrap: nowrap; align-items: center; gap: 8px; padding: 8px 0 10px; margin-bottom: 10px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); overflow-x: auto; }
|
|
381
|
+
.brk-toggle { display: inline-flex; align-items: center; gap: 6px; padding: 5px 11px; border-radius: 6px; font-size: 13px; border: 1px solid var(--line); background: var(--panel); color: var(--muted); transition: .15s; }
|
|
382
|
+
.brk-toggle.on { border-color: rgba(22, 163, 74, 0.55); background: rgba(22, 163, 74, 0.14); color: #4ade80; font-weight: 600; }
|
|
383
|
+
.brk-icon-btn { width: 32px; height: 32px; padding: 0; display: inline-flex; align-items: center; justify-content: center; border-radius: 6px; font-size: 15px; flex: none; }
|
|
384
|
+
tr.day-group-row td { background: var(--panel); color: var(--text); font-weight: 600; font-size: 12.5px; padding: 7px 10px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
|
|
385
|
+
details.hist-year-box { margin-top: 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg); overflow: hidden; }
|
|
386
|
+
details.hist-year-box > summary { padding: 10px 14px; cursor: pointer; font-weight: 600; font-size: 13.5px; background: var(--panel); display: flex; justify-content: space-between; align-items: center; gap: 10px; user-select: none; }
|
|
387
|
+
details.hist-year-box > summary:hover { border-color: var(--accent); color: var(--accent); }
|
|
388
|
+
details.hist-year-box[open] > summary { border-bottom: 1px solid var(--line); }
|
|
389
|
+
@media (max-width: 700px) { .brk-specs { grid-template-columns: 1fr; } }
|
|
335
390
|
|
|
336
391
|
@media (max-width: 900px) {
|
|
337
392
|
body { overflow: auto; }
|
package/server/api/routes.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
|
|
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, acceptUserOffer, createVipRequest, listVipRequests, 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);
|
|
@@ -74,10 +129,19 @@ export function createRouter() {
|
|
|
74
129
|
res.json({ ok: true, ...sent });
|
|
75
130
|
}));
|
|
76
131
|
r.post('/auth/redeem-promo', wrap((req, res) => {
|
|
77
|
-
if (!req.user) throw new Error('Войдите в аккаунт для активации
|
|
132
|
+
if (!req.user) throw new Error('Войдите в аккаунт для активации кода или VIP-токена');
|
|
78
133
|
const user = redeemPromoCode(req.user.id, req.body?.code);
|
|
79
134
|
res.json({ user });
|
|
80
135
|
}));
|
|
136
|
+
r.post('/auth/accept-offer', wrap((req, res) => {
|
|
137
|
+
if (!req.user) throw new Error('Войдите в аккаунт');
|
|
138
|
+
res.json({ ok: true, user: acceptUserOffer(req.user.id) });
|
|
139
|
+
}));
|
|
140
|
+
r.post('/auth/vip-request', wrap(async (req, res) => {
|
|
141
|
+
const item = createVipRequest({ user: req.user, ...(req.body || {}) });
|
|
142
|
+
notifyUser(1, `👑 Заявка на тариф VIP (НейроАльфа):\nИмя: ${item.name}\nКонтакт: ${item.contact || item.email}\nКапитал: ${item.capitalRub || '—'}\nКомментарий: ${item.comment || '—'}`).catch(() => {});
|
|
143
|
+
res.json({ ok: true, request: item });
|
|
144
|
+
}));
|
|
81
145
|
r.post('/auth/api-key/regenerate', wrap((req, res) => {
|
|
82
146
|
if (!req.user) throw new Error('Войдите в аккаунт');
|
|
83
147
|
const apiKey = ensureApiKey(req.user.id, true);
|
|
@@ -103,6 +167,7 @@ export function createRouter() {
|
|
|
103
167
|
// Активация API-ключа облачной подписки на локальном npx-клиенте
|
|
104
168
|
r.post('/license/activate', wrap(async (req, res) => {
|
|
105
169
|
const lic = await activateLocalLicense(req.body?.apiKey, req.body?.cloudUrl);
|
|
170
|
+
llmChanged();
|
|
106
171
|
res.json({ ok: true, license: lic, user: publicUser(req.user) });
|
|
107
172
|
}));
|
|
108
173
|
|
|
@@ -126,49 +191,108 @@ export function createRouter() {
|
|
|
126
191
|
if (pid) await verifyYookassaPayment(pid).catch(() => {});
|
|
127
192
|
res.json({ ok: true });
|
|
128
193
|
}));
|
|
194
|
+
r.post('/notify/max/webhook', wrap(async (req, res) => {
|
|
195
|
+
res.json(await handleMaxWebhook(req.body || {}));
|
|
196
|
+
}));
|
|
129
197
|
|
|
130
198
|
// ---------- публичная витрина для лендинга ----------
|
|
131
199
|
r.get('/public/showcase', (req, res) => {
|
|
132
|
-
const
|
|
133
|
-
const
|
|
200
|
+
const buildShowcaseItem = (x, full) => {
|
|
201
|
+
const chosen = x.metrics?.model?.chosen || 'attention';
|
|
202
|
+
const cmpFull = x.metrics?.model?.comparison?.[chosen]?.full || full?.extra?.comparison?.[chosen]?.full || {};
|
|
203
|
+
const mFull = { ...(x.metrics?.full || {}), ...cmpFull };
|
|
204
|
+
const p = x.params || {};
|
|
205
|
+
const dir = p.modelDirection || 'long';
|
|
206
|
+
const topK = p.modelTopK || 2;
|
|
207
|
+
const from = x.metrics?.period?.from || 0, till = x.metrics?.period?.till || 0;
|
|
208
|
+
const yrs = Math.max(0.5, (till - from) / (365.25 * 86400000));
|
|
209
|
+
const ret = Number(mFull.returnPct || 0);
|
|
210
|
+
const cagr = yrs >= 0.25 && ret > -99 ? ((Math.pow(1 + ret / 100, 1 / yrs) - 1) * 100) : (ret / yrs);
|
|
211
|
+
|
|
212
|
+
let tierBadge = 'Тариф «Инвестор»';
|
|
213
|
+
let tierColor = 'blue';
|
|
214
|
+
let tierNote = 'Режим «Только в рост (без шорта)» · лимит до 50% годовых · до 1 млн ₽ на счёт';
|
|
215
|
+
let autoTitle = p._title;
|
|
216
|
+
if (dir === 'long' || cagr <= 52) {
|
|
217
|
+
tierBadge = 'Для тарифа «Инвестор»';
|
|
218
|
+
tierColor = 'blue';
|
|
219
|
+
tierNote = 'Торговля только от покупки (без шортов) · потолок тарифа до 50% годовых · до 1 млн ₽/счёт';
|
|
220
|
+
if (!autoTitle) autoTitle = 'Консервативный рост · Только покупки';
|
|
221
|
+
} else if (cagr <= 105) {
|
|
222
|
+
tierBadge = 'Для тарифа «Профи»';
|
|
223
|
+
tierColor = 'indigo';
|
|
224
|
+
tierNote = 'Заработок на росте и падении акций (с шортами) · потолок тарифа до 100% годовых · до 10 млн ₽/счёт';
|
|
225
|
+
if (!autoTitle) autoTitle = 'Активный рост и падение · С шортами';
|
|
226
|
+
} else {
|
|
227
|
+
tierBadge = '👑 Для тарифа «VIP»';
|
|
228
|
+
tierColor = 'gold';
|
|
229
|
+
tierNote = 'Полная мощность без потолка доходности · форсаж H/2 + концентрация слота Conviction Sizing';
|
|
230
|
+
if (!autoTitle || autoTitle === 'Эталонная адаптивная стратегия') autoTitle = 'Институциональный форсаж · Полная мощность';
|
|
231
|
+
}
|
|
134
232
|
return {
|
|
135
233
|
id: x.id,
|
|
136
|
-
title:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
234
|
+
title: autoTitle,
|
|
235
|
+
tierBadge,
|
|
236
|
+
tierColor,
|
|
237
|
+
tierNote,
|
|
238
|
+
params: { modelHorizon: p.modelHorizon || 2.5, modelTopK: topK, modelDirection: dir, modelType: chosen },
|
|
239
|
+
metrics: mFull,
|
|
240
|
+
oos: x.metrics?.model?.comparison?.[chosen]?.outOfSample || full?.extra?.comparison?.[chosen]?.outOfSample || {},
|
|
140
241
|
period: x.metrics?.period || {},
|
|
141
|
-
curve: full?.extra?.curves?.[
|
|
242
|
+
curve: full?.extra?.curves?.[chosen]?.slice(-180) || [],
|
|
142
243
|
marketCurve: full?.extra?.curves?.market?.slice(-180) || [],
|
|
143
244
|
};
|
|
144
|
-
}
|
|
245
|
+
};
|
|
246
|
+
const runs = listShowcaseRuns().map((x) => buildShowcaseItem(x, getRun(x.id)));
|
|
145
247
|
if (!runs.length) {
|
|
146
248
|
try {
|
|
147
|
-
const ref = JSON.parse(fs.readFileSync(path.join(ROOT, '
|
|
148
|
-
if (ref
|
|
149
|
-
runs.push({
|
|
150
|
-
id: 234,
|
|
151
|
-
title: 'Эталонная стратегия «Внимание через карту»',
|
|
152
|
-
params: { modelHorizon: 2, modelTopK: ref.params?.topK || 2, modelDirection: 'long', modelType: 'attention' },
|
|
153
|
-
metrics: ref.cmp.attention.full,
|
|
154
|
-
oos: ref.cmp.attention.oos || {},
|
|
155
|
-
period: ref.period || {},
|
|
156
|
-
curve: (ref.curves?.attention || []).slice(-180),
|
|
157
|
-
marketCurve: (ref.curves?.market || []).slice(-180),
|
|
158
|
-
});
|
|
159
|
-
}
|
|
249
|
+
const ref = JSON.parse(fs.readFileSync(path.join(ROOT, 'config', 'seed-run.json'), 'utf8'));
|
|
250
|
+
if (ref) runs.push(buildShowcaseItem({ id: 1, ...ref }, ref));
|
|
160
251
|
} catch { /* ignore */ }
|
|
161
252
|
}
|
|
162
|
-
res.json({ runs, plans: PLANS });
|
|
253
|
+
res.json({ runs, plans: PLANS, legal: getLegalInfo() });
|
|
163
254
|
});
|
|
164
255
|
|
|
165
|
-
// ----------
|
|
256
|
+
// ---------- реквизиты оферты (для модерации ЮKassa и правовых документов) ----------
|
|
257
|
+
const DEFAULT_LEGAL = {
|
|
258
|
+
ownerName: 'ООО «БИС» («Бизнес Интер Софт»)',
|
|
259
|
+
authorName: 'Перепелкин Роман Александрович',
|
|
260
|
+
status: 'Оператор платформы PaaS · Проприетарное ПО (Все права защищены)',
|
|
261
|
+
inn: '',
|
|
262
|
+
ogrn: '',
|
|
263
|
+
email: 'support@neuroalpha.ru',
|
|
264
|
+
phone: '',
|
|
265
|
+
address: 'Российская Федерация, г. Рязань',
|
|
266
|
+
};
|
|
267
|
+
const getLegalInfo = () => ({ ...DEFAULT_LEGAL, ...(kvGet('legalInfo') || {}) });
|
|
268
|
+
r.get('/public/legal', (req, res) => res.json(getLegalInfo()));
|
|
269
|
+
|
|
270
|
+
// ---------- админ-панель (пользователи, оплаты, ЮKassa, боты Telegram и MAX, витрина, реквизиты) ----------
|
|
166
271
|
const requireAdmin = (req) => {
|
|
167
272
|
if (!req.user || req.user.role !== 'admin') throw new Error('Доступно только администратору платформы');
|
|
168
273
|
};
|
|
169
274
|
r.get('/admin/state', wrap((req, res) => {
|
|
170
275
|
requireAdmin(req);
|
|
171
|
-
res.json({ users: listAllUsers(), payments: listAllPayments(), promos: listPromoCodes(), billing: publicBillingConfig(), notify: publicNotifyConfig() });
|
|
276
|
+
res.json({ users: listAllUsers(), payments: listAllPayments(), promos: listPromoCodes(), vipRequests: listVipRequests(), billing: publicBillingConfig(), notify: publicNotifyConfig(), oauth: publicOAuthConfig(), legal: getLegalInfo() });
|
|
277
|
+
}));
|
|
278
|
+
r.post('/admin/oauth', wrap((req, res) => {
|
|
279
|
+
requireAdmin(req);
|
|
280
|
+
res.json({ ok: true, oauth: saveOAuthConfig(req.body || {}) });
|
|
281
|
+
}));
|
|
282
|
+
r.post('/admin/legal', wrap((req, res) => {
|
|
283
|
+
requireAdmin(req);
|
|
284
|
+
const b = req.body || {};
|
|
285
|
+
const next = {
|
|
286
|
+
ownerName: String(b.ownerName ?? '').trim(),
|
|
287
|
+
status: String(b.status ?? '').trim(),
|
|
288
|
+
inn: String(b.inn ?? '').trim(),
|
|
289
|
+
ogrn: String(b.ogrn ?? '').trim(),
|
|
290
|
+
email: String(b.email ?? '').trim(),
|
|
291
|
+
phone: String(b.phone ?? '').trim(),
|
|
292
|
+
address: String(b.address ?? '').trim(),
|
|
293
|
+
};
|
|
294
|
+
kvSet('legalInfo', next);
|
|
295
|
+
res.json({ ok: true, legal: getLegalInfo() });
|
|
172
296
|
}));
|
|
173
297
|
r.post('/admin/promo', wrap((req, res) => {
|
|
174
298
|
requireAdmin(req);
|
|
@@ -182,9 +306,9 @@ export function createRouter() {
|
|
|
182
306
|
requireAdmin(req);
|
|
183
307
|
res.json(saveBillingConfig(req.body || {}));
|
|
184
308
|
}));
|
|
185
|
-
r.post('/admin/notify', wrap((req, res) => {
|
|
309
|
+
r.post('/admin/notify', wrap(async (req, res) => {
|
|
186
310
|
requireAdmin(req);
|
|
187
|
-
res.json(saveNotifyConfig(req.body || {}));
|
|
311
|
+
res.json(await saveNotifyConfig(req.body || {}));
|
|
188
312
|
}));
|
|
189
313
|
r.put('/runs/:id/showcase', wrap((req, res) => {
|
|
190
314
|
requireAdmin(req);
|
|
@@ -205,6 +329,67 @@ export function createRouter() {
|
|
|
205
329
|
r.post('/llm/test', wrap(async (req, res) => res.json(await testProvider(req.body?.providerId, req.body?.model))));
|
|
206
330
|
r.post('/llm/select', wrap((req, res) => { selectModel(req.body?.providerId, req.body?.model); llmChanged(); res.json(llmState()); }));
|
|
207
331
|
|
|
332
|
+
// ---------- облачный ИИ-прокси для локальных npx-клиентов по APIKey или в демо-режиме ----------
|
|
333
|
+
const proxyUsesToday = (ident) => {
|
|
334
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
335
|
+
const key = `llmProxy:${ident}:${day}`;
|
|
336
|
+
return { key, count: Number(kvGet(key, 0)) || 0 };
|
|
337
|
+
};
|
|
338
|
+
r.get('/llm/proxy/v1/models', (req, res) => {
|
|
339
|
+
res.json({ data: [{ id: 'Qwen/Qwen3.8-27B-FP8', object: 'model' }] });
|
|
340
|
+
});
|
|
341
|
+
r.post('/llm/proxy/v1/chat/completions', async (req, res) => {
|
|
342
|
+
try {
|
|
343
|
+
const ip = String(req.headers['x-forwarded-for'] || req.socket?.remoteAddress || 'anon').split(',')[0].trim();
|
|
344
|
+
let maxPerDay = 5;
|
|
345
|
+
let ident = `ip:${ip}`;
|
|
346
|
+
let planLabel = 'Демо (без ключа)';
|
|
347
|
+
if (req.user) {
|
|
348
|
+
const plan = effectivePlan(req.user);
|
|
349
|
+
maxPerDay = plan.maxChatPerDay;
|
|
350
|
+
ident = `u:${req.user.id}`;
|
|
351
|
+
planLabel = plan.name;
|
|
352
|
+
}
|
|
353
|
+
const u = proxyUsesToday(ident);
|
|
354
|
+
if (u.count >= maxPerDay) {
|
|
355
|
+
return res.status(429).json({
|
|
356
|
+
error: {
|
|
357
|
+
message: req.user
|
|
358
|
+
? `Исчерпан дневной лимит ИИ-запросов по тарифу «${planLabel}» (${maxPerDay} в сутки). Расширьте тариф на https://neuroalpha.odant.org`
|
|
359
|
+
: `Исчерпан бесплатный демо-лимит (${maxPerDay} запросов в сутки). Получите бесплатный или платный APIKey на https://neuroalpha.odant.org`,
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
const t = resolveUpstreamForProxy();
|
|
364
|
+
if (!t) {
|
|
365
|
+
return res.status(503).json({ error: { message: 'На сервере neuroalpha.odant.org ещё не подключена апстрим-модель ИИ' } });
|
|
366
|
+
}
|
|
367
|
+
kvSet(u.key, u.count + 1);
|
|
368
|
+
const body = { ...(req.body || {}), model: t.model, stream: true };
|
|
369
|
+
if (!body.reasoning_effort && t.effort) body.reasoning_effort = t.effort;
|
|
370
|
+
const upstream = await providerFetch(t.provider, t.url, {
|
|
371
|
+
method: 'POST',
|
|
372
|
+
headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' },
|
|
373
|
+
body: JSON.stringify(body),
|
|
374
|
+
signal: AbortSignal.timeout(90000),
|
|
375
|
+
});
|
|
376
|
+
if (!upstream.ok) {
|
|
377
|
+
const txt = await upstream.text().catch(() => '');
|
|
378
|
+
return res.status(upstream.status).json({ error: { message: txt.slice(0, 300) || `HTTP ${upstream.status}` } });
|
|
379
|
+
}
|
|
380
|
+
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
|
381
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
382
|
+
res.setHeader('Connection', 'keep-alive');
|
|
383
|
+
for await (const chunk of upstream.body) {
|
|
384
|
+
res.write(chunk);
|
|
385
|
+
}
|
|
386
|
+
res.end();
|
|
387
|
+
} catch (e) {
|
|
388
|
+
if (!res.headersSent) res.status(500).json({ error: { message: e.message } });
|
|
389
|
+
else res.end();
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
208
393
|
// ---------- настройки ----------
|
|
209
394
|
r.get('/settings', (req, res) => res.json(getSettings()));
|
|
210
395
|
r.put('/settings', wrap((req, res) => res.json(updateSettings(req.body || {}, { source: 'user' }))));
|
|
@@ -256,9 +441,6 @@ export function createRouter() {
|
|
|
256
441
|
}
|
|
257
442
|
const body = req.body || {};
|
|
258
443
|
const merged = { ...getSettings(), ...body };
|
|
259
|
-
if ((merged.modelUseTransformer || merged.modelType === 'transformer') && !plan.allowTransformer) {
|
|
260
|
-
throw new Error('Обучение нейросети-трансформера доступно на тарифе «Профи / Лаборатория».');
|
|
261
|
-
}
|
|
262
444
|
const { runId, reused } = startModelResearch(merged, { source: 'user', force: !!body.force });
|
|
263
445
|
res.json({ runId, reused: !!reused });
|
|
264
446
|
}));
|
|
@@ -282,6 +464,7 @@ export function createRouter() {
|
|
|
282
464
|
|
|
283
465
|
// ---------- брокерские счета (Т-Банк, Финам, Алор, БКС) ----------
|
|
284
466
|
r.get('/brokers', (req, res) => res.json(brokersState()));
|
|
467
|
+
r.post('/brokers/discover', wrap(async (req, res) => res.json(await discoverBrokerAccounts(req.body || {}))));
|
|
285
468
|
r.post('/brokers', wrap((req, res) => {
|
|
286
469
|
const plan = effectivePlan(req.user);
|
|
287
470
|
const cur = brokersState().accounts.length;
|
|
@@ -293,6 +476,8 @@ export function createRouter() {
|
|
|
293
476
|
r.put('/brokers/:id', wrap((req, res) => res.json(saveBrokerAccount({ ...(req.body || {}), id: req.params.id }))));
|
|
294
477
|
r.delete('/brokers/:id', wrap((req, res) => res.json(deleteBrokerAccount(req.params.id))));
|
|
295
478
|
r.post('/brokers/:id/sync', wrap(async (req, res) => res.json(await syncBrokerAccount(req.params.id))));
|
|
479
|
+
r.post('/brokers/:id/history', wrap(async (req, res) => res.json(await fetchBrokerHistory(req.params.id))));
|
|
480
|
+
r.post('/brokers/:id/freeze-baseline', wrap(async (req, res) => res.json(await freezeBrokerBaseline(req.params.id))));
|
|
296
481
|
r.post('/brokers/:id/prepare', wrap(async (req, res) => res.json(await prepareBrokerOrders(req.params.id))));
|
|
297
482
|
r.post('/brokers/:id/execute', wrap(async (req, res) => res.json(await executeBrokerOrders(req.params.id))));
|
|
298
483
|
r.post('/brokers/:id/reject', wrap((req, res) => res.json(rejectBrokerOrders(req.params.id))));
|