neuroalpha 0.3.17 → 0.5.1
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/config/certs/russian_trusted_root_ca.pem +33 -0
- package/docs//320/224/320/225/320/237/320/233/320/236/320/231.md +4 -4
- package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +1 -4
- 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 +2 -2
- package/package.json +60 -53
- package/public/activate.html +36 -0
- package/public/activate.js +33 -0
- package/public/admin.html +126 -7
- package/public/app.js +573 -66
- package/public/index.html +48 -21
- package/public/landing.html +106 -37
- package/public/legal.html +4 -4
- package/public/styles.css +152 -36
- package/server/api/routes.js +308 -37
- package/server/auth/index.js +79 -15
- package/server/brokers/index.js +1014 -168
- package/server/build-info.js +7 -0
- package/server/config.js +13 -6
- package/server/data/moex.js +6 -4
- package/server/db.js +22 -18
- package/server/desktop-entry.js +103 -0
- package/server/hub.js +28 -14
- package/server/index.js +82 -12
- package/server/instance/agent.js +259 -0
- package/server/instance/central.js +491 -0
- package/server/instance/identity.js +79 -0
- package/server/instance/provision.js +166 -0
- package/server/instance/signing.js +71 -0
- package/server/model/edge.js +19 -0
- package/server/model/forecast.js +107 -57
- package/server/model/live.js +1 -1
- package/server/model/models.js +12 -3
- package/server/model/research.js +218 -30
- package/server/notify/index.js +51 -16
- package/server/role.js +44 -0
- package/server/security.js +40 -0
- package/server/strategist/index.js +86 -47
- package/server/strategist/tools.js +231 -42
- package/server/tls-ca.js +32 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
|
|
2
|
+
// Защитные HTTP-заголовки, CSP для терминала и защита от межсайтовых запросов (CSRF / Cross-Site WebSocket)
|
|
3
|
+
|
|
4
|
+
const APP_CSP = [
|
|
5
|
+
"default-src 'self'",
|
|
6
|
+
"script-src 'self' https://unpkg.com",
|
|
7
|
+
"style-src 'self' 'unsafe-inline'",
|
|
8
|
+
"img-src 'self' data: https:",
|
|
9
|
+
"font-src 'self' data:",
|
|
10
|
+
"connect-src 'self' ws: wss:",
|
|
11
|
+
"object-src 'none'",
|
|
12
|
+
"base-uri 'self'",
|
|
13
|
+
"form-action 'self'",
|
|
14
|
+
"frame-ancestors 'none'",
|
|
15
|
+
].join('; ');
|
|
16
|
+
|
|
17
|
+
export function securityHeaders(req, res, next) {
|
|
18
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
19
|
+
res.setHeader('Referrer-Policy', 'same-origin');
|
|
20
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
21
|
+
if (req.path === '/index.html') res.setHeader('Content-Security-Policy', APP_CSP);
|
|
22
|
+
next();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// CSP торгового терминала (index.html). На лендинге и в админке есть встроенные скрипты — им CSP не ставится.
|
|
26
|
+
export function setAppCsp(res) {
|
|
27
|
+
res.setHeader('Content-Security-Policy', APP_CSP);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Запрос из браузера с чужого сайта: Origin есть и не совпадает с Host.
|
|
31
|
+
// Запросы без Origin (curl, боты, серверные клиенты API) не браузерные — их защищает авторизация.
|
|
32
|
+
export function isCrossOrigin(req) {
|
|
33
|
+
const origin = req.headers?.origin;
|
|
34
|
+
if (!origin || origin === 'null') return Boolean(origin);
|
|
35
|
+
try {
|
|
36
|
+
return new URL(origin).host.toLowerCase() !== String(req.headers?.host || '').toLowerCase();
|
|
37
|
+
} catch {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -6,7 +6,7 @@ import { getSettings } from '../settings.js';
|
|
|
6
6
|
import { brokersState } from '../brokers/index.js';
|
|
7
7
|
import {
|
|
8
8
|
currentConversation, newConversation, addMessage, updateMessage, getMessage, getMessages,
|
|
9
|
-
kvGet, kvSet,
|
|
9
|
+
kvGet, kvSet, currentUid,
|
|
10
10
|
} from '../db.js';
|
|
11
11
|
import { TOOLS, MODES, toolSchemas, OOS_LIMIT } from './tools.js';
|
|
12
12
|
|
|
@@ -15,30 +15,40 @@ const MSK = 3 * 3600000;
|
|
|
15
15
|
const fmtTs = (ts) => new Date(ts + MSK).toISOString().slice(0, 16).replace('T', ' ');
|
|
16
16
|
const fmtPct = (v) => (v === null || v === undefined ? '—' : `${v > 0 ? '+' : ''}${v}%`);
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
// Состояние чата отдельно для каждого пользователя (uid берётся из контекста запроса)
|
|
19
|
+
const states = new Map();
|
|
20
|
+
function userState() {
|
|
21
|
+
const uid = currentUid();
|
|
22
|
+
let s = states.get(uid);
|
|
23
|
+
if (!s) {
|
|
24
|
+
s = {
|
|
25
|
+
conv: null,
|
|
26
|
+
run: null, // { ctrl, mode }
|
|
27
|
+
waiting: null, // { msgId, callId }
|
|
28
|
+
budgetLeft: 0,
|
|
29
|
+
};
|
|
30
|
+
states.set(uid, s);
|
|
31
|
+
}
|
|
32
|
+
return s;
|
|
33
|
+
}
|
|
24
34
|
|
|
25
35
|
export const getMode = () => 'advisor';
|
|
26
36
|
export const getBudget = () => 8;
|
|
27
37
|
|
|
28
38
|
function conv() {
|
|
29
|
-
if (!
|
|
30
|
-
return
|
|
39
|
+
if (!userState().conv) userState().conv = currentConversation();
|
|
40
|
+
return userState().conv;
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
export function chatState() {
|
|
34
44
|
const llm = activeModelInfo();
|
|
35
45
|
return {
|
|
36
46
|
convId: conv().id,
|
|
37
|
-
busy: !!
|
|
38
|
-
waiting:
|
|
47
|
+
busy: !!userState().run,
|
|
48
|
+
waiting: userState().waiting,
|
|
39
49
|
mode: 'advisor',
|
|
40
50
|
budget: 8,
|
|
41
|
-
budgetLeft:
|
|
51
|
+
budgetLeft: userState().budgetLeft,
|
|
42
52
|
llm: llm.enabled,
|
|
43
53
|
tools: llm.tools,
|
|
44
54
|
oosUsed: 0,
|
|
@@ -63,26 +73,53 @@ function systemPrompt() {
|
|
|
63
73
|
} catch { /* ignore */ }
|
|
64
74
|
|
|
65
75
|
const noTools = !activeModelInfo().tools;
|
|
76
|
+
const nowTs = Date.now();
|
|
77
|
+
const nowMsk = new Date(nowTs + MSK);
|
|
78
|
+
const DAYS_RU = ['воскресенье', 'понедельник', 'вторник', 'среда', 'четверг', 'пятница', 'суббота'];
|
|
79
|
+
const todayIso = nowMsk.toISOString().slice(0, 10);
|
|
80
|
+
const yesterdayIso = new Date(nowTs + MSK - 86400000).toISOString().slice(0, 10);
|
|
81
|
+
const fmtRuDate = (iso) => iso.split('-').reverse().join('.');
|
|
82
|
+
const nowClock = nowMsk.toISOString().slice(11, 16);
|
|
83
|
+
const dow = nowMsk.getUTCDay();
|
|
84
|
+
const mins = nowMsk.getUTCHours() * 60 + nowMsk.getUTCMinutes();
|
|
85
|
+
const moexStatus = (dow === 0 || dow === 6)
|
|
86
|
+
? 'выходной день (основные торги закрыты)'
|
|
87
|
+
: (mins >= 9 * 60 + 50 && mins <= 18 * 60 + 50)
|
|
88
|
+
? 'идёт основная торговая сессия Мосбиржи (09:50–18:50 МСК)'
|
|
89
|
+
: (mins > 18 * 60 + 50 && mins <= 23 * 60 + 50)
|
|
90
|
+
? 'идёт вечерняя торговая сессия Мосбиржи (19:00–23:50 МСК)'
|
|
91
|
+
: 'межсессионный перерыв / торги ещё не открылись';
|
|
92
|
+
|
|
66
93
|
const modeText = noTools
|
|
67
94
|
? 'Выбранная модель ИИ не поддерживает вызов инструментов — ты отвечаешь и консультируешь по данным из контекста ниже.'
|
|
68
|
-
: 'Ты можешь свободно вызывать читающие инструменты (`get_brokers_status`, `model_forecast`, `get_news`, `market_neighbors`, `get_settings`, `broker_onboarding_guide`, `discover_broker_accounts`, `analyze_run`), а действия `connect_broker_account` и `set_settings` предлагаешь с подтверждением пользователя кнопкой.';
|
|
95
|
+
: 'Ты можешь свободно вызывать читающие инструменты (`get_brokers_status`, `get_broker_operations`, `model_forecast`, `get_news`, `market_neighbors`, `get_settings`, `broker_onboarding_guide`, `discover_broker_accounts`, `analyze_run`), а действия `connect_broker_account` и `set_settings` предлагаешь с подтверждением пользователя кнопкой.';
|
|
69
96
|
|
|
70
97
|
return `Ты — «Стратег», персональный ИИ-консультант и помощник пользователя в облачной платформе алгоритмической торговли «НейроАльфа» (Московская биржа, MOEX).
|
|
71
|
-
Твоя задача — простым, понятным русским языком объяснять пользователю прогнозы и сигналы модели, разбирать состояние его брокерских
|
|
72
|
-
ВАЖНО: Ты НЕ ставишь эксперименты и НЕ подбираешь параметры математического ядра —
|
|
98
|
+
Твоя задача — простым, понятным русским языком объяснять пользователю прогнозы и сигналы модели, разбирать состояние его брокерских счетов, портфеля и историю сделок, объяснять новости рынка и помогать с подключением и настройкой счетов.
|
|
99
|
+
ВАЖНО: Ты НЕ ставишь эксперименты и НЕ подбираешь параметры математического ядра — оно работает в зафиксированном автоматическом режиме.
|
|
100
|
+
ЗАПРЕЩЕНО обещать или прогнозировать доходность («X% годовых», «гарантированно», «без риска»), называть цифры бэктеста ожидаемым доходом и давать индивидуальные инвестиционные рекомендации. Если спрашивают про доходность — объясни честно: на истории (бэктест с реальными издержками ~0,1% на сторону, задержкой исполнения и ограничением по ликвидности) стратегия давала порядка десятков процентов годовых при просадках до ~30%, но прошлые результаты не гарантируют будущих, возможны убытки, а при росте издержек выше ~0,17% на сторону стратегия убыточна.
|
|
101
|
+
|
|
102
|
+
## Текущее время и календарь Мосбиржи (МСК, UTC+3)
|
|
103
|
+
- **Сейчас:** ${DAYS_RU[dow]}, **${fmtRuDate(todayIso)}** (${todayIso}), **${nowClock} МСК** — ${moexStatus}.
|
|
104
|
+
- **Сегодняшняя дата:** ${fmtRuDate(todayIso)} (${todayIso}).
|
|
105
|
+
- **Вчерашняя дата:** ${fmtRuDate(yesterdayIso)} (${yesterdayIso}).
|
|
106
|
+
- **ВАЖНОЕ ПРАВИЛО ПО ВРЕМЕНИ И ТОРГАМ:**
|
|
107
|
+
- Инструмент \`get_brokers_status\` показывает **только текущий снимок портфеля прямо сейчас** (какие бумаги лежат на счёте в эту минуту и какие заявки висят в стакане сейчас). Никогда не выдавай текущий портфель из \`get_brokers_status\` за «итоги вчерашних торгов»!
|
|
108
|
+
- Если пользователь спрашивает **«как прошли вчерашние торги»**, **«какие сделки были вчера / сегодня / на неделе»** или просит отчёт по прошлым сделкам — **ОБЯЗАТЕЛЬНО вызывай инструмент \`get_broker_operations\`** (например \`{"period": "yesterday"}\` для вчерашнего дня или \`{"period": "today"}\` для сегодняшнего дня)!
|
|
73
109
|
|
|
74
110
|
## Как устроено автоматическое ядро платформы «НейроАльфа» (Рынок как язык)
|
|
75
111
|
- **Словарь:** ~100 самых ликвидных акций индекса широкого рынка Мосбиржи (MOEXBMI) + индекс IMOEX. Свечи 10-минутные, дивидендные отсечки скорректированы со сверкой по реальному гэпу цены, объёмы нормированы по 24-часовому суточному профилю (valRatio по часам МСК).
|
|
76
112
|
- **Карта рынка и авто-эмбеддинг (modelEmbedDim = 0 Авто):** окно обучения фиксировано на 90 торговых днях. На каждом шаге размерность карты рынка автоматически определяется по спектральному порогу Марченко–Пастура (отсечение рыночного шума от истинных факторов).
|
|
77
113
|
- **12 голов внимания и мульти-горизонт (1,5–3,5 часа):** модель смотрит на 3 масштаба времени (1ч, 3ч, день), цену и денежный поток, узких и широких соседей по карте, предсказывая чистое остаточное движение каждой акции относительно индекса Мосбиржи.
|
|
78
114
|
- **Пороги входа и удержания:**
|
|
79
|
-
-
|
|
80
|
-
- Авто-балансировка по тренду индекса:
|
|
81
|
-
- Удержание позиции: первые 65 минут после входа действует мягкий порог удержания (0,35× от
|
|
115
|
+
- Порог выгоды на вход = 2,5 × (удвоенная сумма комиссии и проскальзывания) × 2 (запас над реальными издержками по стресс-тесту и фактическим сделкам). При издержках 0,05% за сделку это +0,50%.
|
|
116
|
+
- Авто-балансировка по тренду индекса: на растущем рынке (индекс выше 20-дневной средней) покупки открываются от +0,50%, а контртрендовые шорты ужесточаются в 1,25×. На падающем рынке — наоборот.
|
|
117
|
+
- Удержание позиции: первые 65 минут после входа действует мягкий порог удержания (0,35× от порога входа), затем — плотный порог фиксации прибыли.
|
|
118
|
+
- Реальные издержки на счёте Т-Банка (по фактическим сделкам): комиссия ~0,04% + отклонение цены исполнения от цены сигнала ~0,03–0,08% на сторону.
|
|
82
119
|
- **Dynamic Top-K (modelTopK = 0 Авто) и Conviction Sizing:** фиксированного числа акций нет — робот держит от 0 до 4 лонгов и до 4 шортов в зависимости от силы сигналов и концентрирует слот в лучших лидерах.
|
|
83
120
|
|
|
84
121
|
## Брокерские счета и режимы работы
|
|
85
|
-
|
|
122
|
+
Полная поддержка — Т-Банк (T-Invest API, включая секцию TQBR и SPBRU для SPBE). Финам, БКС, Алор, Цифра брокер и ITI Capital — бета, только «Подтверждение заявок». СберИнвестиции, ВТБ и Альфа-Инвестиции сейчас не подключаются (нет открытого API для заявок).
|
|
86
123
|
- **Режимы работы счёта (mode):**
|
|
87
124
|
1. readonly («Только чтение») — безопасный мониторинг портфеля и расчёт заявок без отправки в стакан.
|
|
88
125
|
2. confirm («Подтверждение заявок», рекомендуется) — робот считает лимитные заявки в целых лотах и присылает их в веб-терминал и в Telegram / MAX на подтверждение одной кнопкой.
|
|
@@ -94,10 +131,11 @@ function systemPrompt() {
|
|
|
94
131
|
3. takeover («Полное управление») — сразу продать все старые нецелевые акции из словаря стратегии (облигации, фонды и бумаги вне словаря не трогаются никогда), освободив весь кэш под стратегию.
|
|
95
132
|
|
|
96
133
|
## Тарифные планы SaaS
|
|
97
|
-
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
134
|
+
Тарифы различаются возможностями, а не доходностью (доходность тарифом не определяется и не гарантируется):
|
|
135
|
+
- **«Инвестор» (3 900 ₽/мес):** только покупки (без шорта), 1 брокерский счёт со стартовым лимитом до 1 000 000 ₽ (+ реинвестирование прибыли).
|
|
136
|
+
- **«Профи» (9 900 ₽/мес):** покупки и шорты, до 5 брокерских счетов со стартовым лимитом до 1 000 000 ₽ на каждом.
|
|
137
|
+
- **«VIP» (по договору):** покупки и шорты, количество счетов и сумма по договору, облачная машина или установка на свой компьютер.
|
|
138
|
+
- **Режим «Хозяин» (owner):** без ограничений по счетам и капиталу + доступ к разделу «Эксперименты».
|
|
101
139
|
|
|
102
140
|
## Подключённые брокерские счета пользователя
|
|
103
141
|
${brkSummary}
|
|
@@ -107,9 +145,10 @@ ${brkSummary}
|
|
|
107
145
|
|
|
108
146
|
## Доступные тебе инструменты
|
|
109
147
|
${modeText}
|
|
110
|
-
- Чтобы разобрать портфель
|
|
148
|
+
- Чтобы разобрать текущий портфель пользователя прямо сейчас, свободные рубли или активные заявки в стакане — вызови \`get_brokers_status\`.
|
|
149
|
+
- Чтобы посмотреть, как прошли торги вчера, сегодня или на неделе (история исполненных сделок, покупок, продаж, комиссий и оборота по времени МСК) — вызови \`get_broker_operations\` (например \`{"period": "yesterday"}\` или \`{"period": "today"}\`).
|
|
111
150
|
- Чтобы объяснить текущую ситуацию на рынке — вызови \`model_forecast\` и \`get_news\`.
|
|
112
|
-
- Чтобы помочь подключить счёт — используй \`broker_onboarding_guide\`, \`discover_broker_accounts\` (
|
|
151
|
+
- Чтобы помочь подключить счёт — используй \`broker_onboarding_guide\`, \`connect_broker_account\` и \`discover_broker_accounts\` (по id уже сохранённого счёта). НИКОГДА не проси прислать токен брокера в чат: токен пользователь вставляет только в форму счёта («+ Добавить счёт» → поле «Токен»). Если пользователь всё же прислал токен в чат — не повторяй его в ответе, посоветуй перевыпустить токен у брокера и вставить новый в форму. Полностью поддерживается Т-Банк; Финам, БКС, Алор, Цифра и ITI — бета, только с подтверждением заявок; СберИнвестиции, ВТБ и Альфа сейчас не подключаются.
|
|
113
152
|
|
|
114
153
|
## Как отвечать
|
|
115
154
|
- Размышляй и пиши по-русски, доброжелательно, чётко и простым языком без лишнего жаргона.
|
|
@@ -228,13 +267,13 @@ async function processCalls(msg, mode) {
|
|
|
228
267
|
const mObj = MODES[mode] || MODES.advisor;
|
|
229
268
|
for (const call of msg.tool_calls || []) {
|
|
230
269
|
if (done.has(call.id)) continue;
|
|
231
|
-
if (
|
|
270
|
+
if (userState().run?.ctrl.signal.aborted) return 'aborted';
|
|
232
271
|
const tool = TOOLS[call.function.name];
|
|
233
272
|
const st = getMessage(msg.id).data?.tools?.[call.id]?.status;
|
|
234
273
|
if (tool && mObj.confirm?.(tool) && st !== 'approved') {
|
|
235
274
|
const args = parseArgs(call) || {};
|
|
236
275
|
setToolStatus(msg.id, call.id, { status: 'pending', preview: tool.preview ? tool.preview(args) : '' });
|
|
237
|
-
|
|
276
|
+
userState().waiting = { msgId: msg.id, callId: call.id };
|
|
238
277
|
return 'waiting';
|
|
239
278
|
}
|
|
240
279
|
await execTool(msg, call, mode);
|
|
@@ -243,7 +282,7 @@ async function processCalls(msg, mode) {
|
|
|
243
282
|
}
|
|
244
283
|
|
|
245
284
|
async function loop(mode = 'advisor') {
|
|
246
|
-
const ctrl =
|
|
285
|
+
const ctrl = userState().run.ctrl;
|
|
247
286
|
if (!activeModelInfo().tools) mode = 'discuss';
|
|
248
287
|
for (let step = 0; step < MAX_STEPS; step++) {
|
|
249
288
|
if (ctrl.signal.aborted) return;
|
|
@@ -297,9 +336,9 @@ async function loop(mode = 'advisor') {
|
|
|
297
336
|
}
|
|
298
337
|
|
|
299
338
|
async function runAgent(mode = 'advisor') {
|
|
300
|
-
if (
|
|
339
|
+
if (userState().run) throw new Error('ИИ уже работает');
|
|
301
340
|
if (!isLlmEnabled()) throw new Error('ИИ не подключён: выберите модель в разделе «Модели ИИ»');
|
|
302
|
-
|
|
341
|
+
userState().run = { ctrl: new AbortController(), mode };
|
|
303
342
|
emitState();
|
|
304
343
|
try {
|
|
305
344
|
await loop(mode);
|
|
@@ -307,7 +346,7 @@ async function runAgent(mode = 'advisor') {
|
|
|
307
346
|
console.error(e);
|
|
308
347
|
emitMsg(addMessage(conv().id, { role: 'assistant', kind: 'assistant', content: `⚠️ Ошибка: ${e.message}` }));
|
|
309
348
|
} finally {
|
|
310
|
-
|
|
349
|
+
userState().run = null;
|
|
311
350
|
emitState();
|
|
312
351
|
}
|
|
313
352
|
}
|
|
@@ -316,8 +355,8 @@ async function runAgent(mode = 'advisor') {
|
|
|
316
355
|
export function sendUserMessage(text) {
|
|
317
356
|
text = String(text || '').trim();
|
|
318
357
|
if (!text) throw new Error('Пустое сообщение');
|
|
319
|
-
if (
|
|
320
|
-
if (
|
|
358
|
+
if (userState().run) throw new Error('ИИ ещё работает — дождитесь или нажмите «Стоп»');
|
|
359
|
+
if (userState().waiting) rejectPending('Пользователь не стал подтверждать и написал сообщение (см. ниже).');
|
|
321
360
|
const m = addMessage(conv().id, { role: 'user', kind: 'user', content: text });
|
|
322
361
|
emitMsg(m);
|
|
323
362
|
runAgent('advisor').catch(() => {});
|
|
@@ -325,9 +364,9 @@ export function sendUserMessage(text) {
|
|
|
325
364
|
}
|
|
326
365
|
|
|
327
366
|
function rejectPending(comment) {
|
|
328
|
-
const w =
|
|
367
|
+
const w = userState().waiting;
|
|
329
368
|
if (!w) return;
|
|
330
|
-
|
|
369
|
+
userState().waiting = null;
|
|
331
370
|
const msg = getMessage(w.msgId);
|
|
332
371
|
for (const call of msg.tool_calls || []) {
|
|
333
372
|
const st = msg.data?.tools?.[call.id]?.status;
|
|
@@ -339,23 +378,23 @@ function rejectPending(comment) {
|
|
|
339
378
|
}
|
|
340
379
|
|
|
341
380
|
export function resolveAction(msgId, callId, decision, comment = '') {
|
|
342
|
-
const w =
|
|
381
|
+
const w = userState().waiting;
|
|
343
382
|
if (!w || w.msgId !== msgId || w.callId !== callId) throw new Error('Это действие уже не ожидает подтверждения');
|
|
344
|
-
if (
|
|
383
|
+
if (userState().run) throw new Error('ИИ ещё работает');
|
|
345
384
|
const mode = 'advisor';
|
|
346
385
|
if (decision === 'approve') {
|
|
347
|
-
|
|
386
|
+
userState().waiting = null;
|
|
348
387
|
setToolStatus(msgId, callId, { status: 'approved' });
|
|
349
388
|
} else {
|
|
350
389
|
const msg = getMessage(msgId);
|
|
351
|
-
|
|
390
|
+
userState().waiting = null;
|
|
352
391
|
const call = msg.tool_calls.find((c) => c.id === callId);
|
|
353
392
|
const tm = addMessage(conv().id, { role: 'tool', kind: 'tool', tool_call_id: callId, content: JSON.stringify({ отклонено: true, комментарий: comment || 'пользователь отклонил без комментария' }), data: { name: call?.function.name, summary: `Отклонено${comment ? `: ${comment}` : ''}`, ok: false } });
|
|
354
393
|
emitMsg(tm);
|
|
355
394
|
setToolStatus(msgId, callId, { status: 'rejected' });
|
|
356
395
|
}
|
|
357
396
|
if (!isLlmEnabled()) throw new Error('ИИ не подключён');
|
|
358
|
-
|
|
397
|
+
userState().run = { ctrl: new AbortController(), mode };
|
|
359
398
|
emitState();
|
|
360
399
|
(async () => {
|
|
361
400
|
try {
|
|
@@ -364,17 +403,17 @@ export function resolveAction(msgId, callId, decision, comment = '') {
|
|
|
364
403
|
} catch (e) {
|
|
365
404
|
emitMsg(addMessage(conv().id, { role: 'assistant', kind: 'assistant', content: `⚠️ Ошибка: ${e.message}` }));
|
|
366
405
|
} finally {
|
|
367
|
-
|
|
406
|
+
userState().run = null;
|
|
368
407
|
emitState();
|
|
369
408
|
}
|
|
370
409
|
})();
|
|
371
410
|
}
|
|
372
411
|
|
|
373
412
|
export function stopAgent() {
|
|
374
|
-
if (
|
|
375
|
-
|
|
413
|
+
if (userState().run) {
|
|
414
|
+
userState().run.ctrl.abort();
|
|
376
415
|
}
|
|
377
|
-
if (
|
|
416
|
+
if (userState().waiting) rejectPending('Пользователь остановил ИИ');
|
|
378
417
|
emitState();
|
|
379
418
|
}
|
|
380
419
|
|
|
@@ -385,10 +424,10 @@ export function setMode() {
|
|
|
385
424
|
|
|
386
425
|
export function resetConversation() {
|
|
387
426
|
stopAgent();
|
|
388
|
-
|
|
389
|
-
|
|
427
|
+
userState().conv = newConversation();
|
|
428
|
+
userState().waiting = null;
|
|
390
429
|
hub.emit({ type: 'chat:reset', state: chatState() });
|
|
391
|
-
return
|
|
430
|
+
return userState().conv;
|
|
392
431
|
}
|
|
393
432
|
|
|
394
433
|
export function history() {
|