neuroalpha 0.5.12 → 0.5.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/package.json +1 -1
- package/public/app.js +2 -1
- package/server/index.js +6 -2
- package/server/instance/central.js +13 -0
- package/server/notify/index.js +124 -7
- package/server/strategist/index.js +114 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neuroalpha",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.13",
|
|
4
4
|
"description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
|
|
5
5
|
"author": "Roman A. Perepelkin",
|
|
6
6
|
"license": "UNLICENSED",
|
package/public/app.js
CHANGED
|
@@ -3308,9 +3308,10 @@ async function renderCloudPane() {
|
|
|
3308
3308
|
</div>
|
|
3309
3309
|
<div class="srow sblock">
|
|
3310
3310
|
<div class="stxt">
|
|
3311
|
-
<div class="slbl">Уведомления
|
|
3311
|
+
<div class="slbl">Уведомления и общение со Стратегом в Telegram и MAX</div>
|
|
3312
3312
|
<div class="hint">
|
|
3313
3313
|
Ваш код привязки: <b><code>${esc(code)}</code></b> — отправьте этот код боту в Telegram или в мессенджер MAX.<br>
|
|
3314
|
+
После привязки туда приходят сигналы и кнопки подтверждения заявок, а также вы можете писать боту любые вопросы ИИ-Стратегу (или команды <code>/forecast</code>, <code>/portfolio</code>, <code>/today</code>, <code>/news</code>).<br>
|
|
3314
3315
|
Статус: Telegram — ${u.tgConnected ? '<b class="pos">подключён ✓</b>' : '<span class="muted">не привязан</span>'} · MAX — ${u.maxConnected ? '<b class="pos">подключён ✓</b>' : '<span class="muted">не привязан</span>'}
|
|
3315
3316
|
</div>
|
|
3316
3317
|
</div>
|
package/server/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import { runningResearch, runningStability } from './model/research.js';
|
|
|
21
21
|
import { startNews } from './news/index.js';
|
|
22
22
|
import { startNotifyPolling } from './notify/index.js';
|
|
23
23
|
import { startBrokerScheduler, brokersState, executeBrokerOrders, rejectBrokerOrders } from './brokers/index.js';
|
|
24
|
-
import { chatState } from './strategist/index.js';
|
|
24
|
+
import { chatState, askFromMessenger } from './strategist/index.js';
|
|
25
25
|
import { hub } from './hub.js';
|
|
26
26
|
|
|
27
27
|
markStaleRuns();
|
|
@@ -61,8 +61,12 @@ wss.on('connection', (ws, req) => {
|
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
//
|
|
64
|
+
// Команды из Telegram / MAX, пришедшие с центра: подтверждение пакета заявок или вопрос к Стратегу
|
|
65
65
|
setInstanceCommandHandler((cmd) => runWithUser(getUserById(1), async () => {
|
|
66
|
+
if (cmd.type === 'chat' && cmd.text) {
|
|
67
|
+
const res = await askFromMessenger(cmd.text, { channel: cmd.channel || 'tg' });
|
|
68
|
+
return `🤖 Стратег:\n\n${String(res?.text || 'Готово.').slice(0, 3400)}`;
|
|
69
|
+
}
|
|
66
70
|
if (cmd.type !== 'batch') return null;
|
|
67
71
|
const acc = brokersState().accounts.find((a) => a.id === cmd.accountId);
|
|
68
72
|
if (!acc) return 'Команда из мессенджера: счёт не найден.';
|
|
@@ -407,6 +407,19 @@ registerInstanceActionHandler(async (actionId, { channel, chatId }) => {
|
|
|
407
407
|
return `${a.action === 'exec' ? '✓ Команда «Исполнить»' : '✕ Команда «Отклонить»'} отправлена на машину${online ? ' — выполнится в течение нескольких секунд' : ' (сейчас она не на связи — команда выполнится при подключении, в течение 2 часов)'}.`;
|
|
408
408
|
});
|
|
409
409
|
|
|
410
|
+
// Доставка текстового вопроса из Telegram / MAX на выделенную машину клиента (если она есть и на связи)
|
|
411
|
+
export function queueInstanceChatCommand(userId, text, channel = 'tg') {
|
|
412
|
+
const list = instancesOfUser(userId).filter((i) => !i.revoked);
|
|
413
|
+
if (!list.length) return null;
|
|
414
|
+
// предпочитаем машину, которая недавно выходила на связь
|
|
415
|
+
const inst = list.find((i) => i.last_seen && Date.now() - i.last_seen < 3 * 60000) || list[0];
|
|
416
|
+
const cmdId = crypto.randomBytes(9).toString('base64url');
|
|
417
|
+
db.prepare('INSERT INTO instance_commands (id, instance_id, payload, created_at) VALUES (?, ?, ?, ?)')
|
|
418
|
+
.run(cmdId, inst.id, JSON.stringify({ id: cmdId, type: 'chat', text: String(text || '').slice(0, 1500), channel }), Date.now());
|
|
419
|
+
const online = Boolean(inst.last_seen && Date.now() - inst.last_seen < 3 * 60000);
|
|
420
|
+
return { instanceId: inst.id, online };
|
|
421
|
+
}
|
|
422
|
+
|
|
410
423
|
// Команды для машины: отдаются в каждой лицензии, пока машина не подтвердит выполнение (ack)
|
|
411
424
|
function pendingCommands(instId, acked = []) {
|
|
412
425
|
if (acked.length) {
|
package/server/notify/index.js
CHANGED
|
@@ -6,12 +6,16 @@ import { IS_INSTANCE } from '../role.js';
|
|
|
6
6
|
import { getIdentity } from '../instance/identity.js';
|
|
7
7
|
import { relayNotifyToCentral } from '../instance/agent.js';
|
|
8
8
|
import { db, kvGet, kvSet } from '../db.js';
|
|
9
|
-
import { encryptSecret, decryptSecret, getUserById, runWithUser, generateApiKey, hashPassword, ensureApiKey } from '../auth/index.js';
|
|
9
|
+
import { encryptSecret, decryptSecret, getUserById, runWithUser, generateApiKey, hashPassword, ensureApiKey, effectivePlan, countUserChatToday } from '../auth/index.js';
|
|
10
|
+
import { queueInstanceChatCommand } from '../instance/central.js';
|
|
10
11
|
import { round } from '../util.js';
|
|
11
12
|
|
|
12
13
|
let actionHandlers = { execute: null, reject: null };
|
|
13
14
|
export function registerBrokerHandlers(h) { actionHandlers = h; }
|
|
14
15
|
|
|
16
|
+
let chatHandlers = { ask: null, resolve: null };
|
|
17
|
+
export function registerChatHandlers(h) { chatHandlers = h; }
|
|
18
|
+
|
|
15
19
|
// Надёжный HTTP-клиент для platform-api2.max.ru (поддерживает сертификат НУЦ Минцифры РФ)
|
|
16
20
|
const maxCa = TRUSTED_CA;
|
|
17
21
|
|
|
@@ -206,7 +210,97 @@ function tryLinkByCode(text, channel, chatId) {
|
|
|
206
210
|
return u;
|
|
207
211
|
}
|
|
208
212
|
|
|
209
|
-
//
|
|
213
|
+
// Очистка Markdown-разметки под приятный для чтения текст в мессенджере и разбивка длинных ответов на части
|
|
214
|
+
function formatMessengerChunks(raw = '') {
|
|
215
|
+
const cleaned = String(raw || '')
|
|
216
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
217
|
+
.replace(/^#{1,4}\s+/gm, '▸ ')
|
|
218
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
219
|
+
.trim();
|
|
220
|
+
if (!cleaned) return ['Готово.'];
|
|
221
|
+
const MAX_LEN = 3600;
|
|
222
|
+
if (cleaned.length <= MAX_LEN) return [cleaned];
|
|
223
|
+
const chunks = [];
|
|
224
|
+
let rest = cleaned;
|
|
225
|
+
while (rest.length > MAX_LEN) {
|
|
226
|
+
let cut = rest.lastIndexOf('\n\n', MAX_LEN);
|
|
227
|
+
if (cut < 800) cut = rest.lastIndexOf('\n', MAX_LEN);
|
|
228
|
+
if (cut < 800) cut = MAX_LEN;
|
|
229
|
+
chunks.push(rest.slice(0, cut).trim());
|
|
230
|
+
rest = rest.slice(cut).trim();
|
|
231
|
+
}
|
|
232
|
+
if (rest) chunks.push(rest);
|
|
233
|
+
return chunks;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function sendTelegramTyping(chatId) {
|
|
237
|
+
const { tgToken } = getNotifyConfig();
|
|
238
|
+
if (!tgToken || !chatId) return;
|
|
239
|
+
await fetch(`https://api.telegram.org/bot${tgToken}/sendChatAction`, {
|
|
240
|
+
method: 'POST',
|
|
241
|
+
headers: { 'Content-Type': 'application/json' },
|
|
242
|
+
body: JSON.stringify({ chat_id: chatId, action: 'typing' }),
|
|
243
|
+
signal: AbortSignal.timeout(5000),
|
|
244
|
+
}).catch(() => null);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Обработка текстового вопроса к ИИ-Стратегу из Telegram или MAX
|
|
248
|
+
async function handleMessengerChat(text, channel, chatId, sendFn) {
|
|
249
|
+
const col = channel === 'max' ? 'max_chat_id' : 'tg_chat_id';
|
|
250
|
+
const u = db.prepare(`SELECT * FROM users WHERE ${col} = ?`).get(String(chatId));
|
|
251
|
+
if (!u) {
|
|
252
|
+
await sendFn('◈ Чат ещё не привязан к аккаунту НейроАльфа.\n\nОтправьте сюда ваш код привязки (вида NA-XXXXXX) из раздела «Подписка и уведомления» в терминале — после этого вы сможете задавать вопросы ИИ-Стратегу прямо здесь.');
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (/^(\/start|\/help|помощь|справка)$/i.test(text)) {
|
|
256
|
+
await sendFn(
|
|
257
|
+
`◈ НейроАльфа · ИИ-Стратег на связи (${u.name || u.email}).\n\n` +
|
|
258
|
+
'Напишите любой вопрос обычным текстом (например: «Что по SBER?», «Разбери мой портфель», «Какие сделки были вчера?») или используйте быстрые команды:\n' +
|
|
259
|
+
'• /forecast (или «Прогноз») — свежий прогноз и сигналы\n' +
|
|
260
|
+
'• /portfolio (или «Портфель») — состояние счетов и маржи\n' +
|
|
261
|
+
'• /today (или «Сегодня») — сделки и результат за день\n' +
|
|
262
|
+
'• /news (или «Новости») — главные новости рынка\n' +
|
|
263
|
+
'• /new <вопрос> — начать новую тему в архиве',
|
|
264
|
+
);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const plan = effectivePlan(u);
|
|
268
|
+
// Если у пользователя нет локальных брокерских счетов на центральном сервере, но есть выделенная машина (cloud / desktop),
|
|
269
|
+
// пересылаем вопрос на его машину, где хранятся его счета и прогнозы!
|
|
270
|
+
const hasLocalAccounts = Boolean(
|
|
271
|
+
db.prepare('SELECT 1 FROM broker_accounts WHERE user_id = ? LIMIT 1').get(u.id),
|
|
272
|
+
);
|
|
273
|
+
if (!hasLocalAccounts && u.role !== 'admin') {
|
|
274
|
+
const relayed = queueInstanceChatCommand(u.id, text, channel);
|
|
275
|
+
if (relayed) {
|
|
276
|
+
await sendFn(relayed.online
|
|
277
|
+
? '⏳ Запрос передан Стратегу на вашу машину — ответ придёт сюда через несколько секунд…'
|
|
278
|
+
: '⏳ Запрос поставлен в очередь вашей машины (сейчас она не на связи — Стратег ответит при подключении).');
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (!chatHandlers.ask) return;
|
|
283
|
+
if (channel === 'tg') sendTelegramTyping(chatId);
|
|
284
|
+
try {
|
|
285
|
+
const res = await runWithUser(u, async () => {
|
|
286
|
+
if (countUserChatToday() >= (plan.maxChatPerDay || 50)) {
|
|
287
|
+
throw new Error(`Достигнут дневной лимит сообщений Стратегу по тарифу «${plan.name}» (${plan.maxChatPerDay} в сутки).`);
|
|
288
|
+
}
|
|
289
|
+
return await chatHandlers.ask(text, { channel });
|
|
290
|
+
});
|
|
291
|
+
const chunks = formatMessengerChunks(res?.text || '');
|
|
292
|
+
const buttons = res?.waiting ? [
|
|
293
|
+
{ label: '✓ Применить', data: `aiact:approve:${u.id}:${res.waiting.msgId}:${res.waiting.callId}` },
|
|
294
|
+
{ label: '✕ Отклонить', data: `aiact:reject:${u.id}:${res.waiting.msgId}:${res.waiting.callId}` },
|
|
295
|
+
] : null;
|
|
296
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
297
|
+
const isLast = i === chunks.length - 1;
|
|
298
|
+
await sendFn(`🤖 Стратег:\n\n${chunks[i]}`, isLast ? buttons : null);
|
|
299
|
+
}
|
|
300
|
+
} catch (e) {
|
|
301
|
+
await sendFn(`⚠️ Стратег: ${e.message}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
210
304
|
let instanceActionHandler = null;
|
|
211
305
|
export function registerInstanceActionHandler(fn) { instanceActionHandler = fn; }
|
|
212
306
|
|
|
@@ -220,6 +314,24 @@ async function handleCallbackAction(data, replyFn, { channel, chatId } = {}) {
|
|
|
220
314
|
}
|
|
221
315
|
return;
|
|
222
316
|
}
|
|
317
|
+
if (String(data || '').startsWith('aiact:')) {
|
|
318
|
+
const [, decision, uidStr, msgIdStr, callId] = String(data).split(':');
|
|
319
|
+
const u = getUserById(Number(uidStr));
|
|
320
|
+
if (!u || !chatHandlers.resolve) return;
|
|
321
|
+
const boundChat = channel === 'max' ? u.max_chat_id : u.tg_chat_id;
|
|
322
|
+
if (!boundChat || !chatId || String(boundChat) !== String(chatId)) {
|
|
323
|
+
await replyFn('⛔ Это действие доступно только из привязанного чата владельца.');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
const res = await runWithUser(u, () => chatHandlers.resolve(Number(msgIdStr), String(callId), decision));
|
|
328
|
+
const chunks = formatMessengerChunks(res?.text || '');
|
|
329
|
+
for (const ch of chunks) await replyFn(`🤖 Стратег:\n\n${ch}`);
|
|
330
|
+
} catch (e) {
|
|
331
|
+
await replyFn(`⚠️ ${e.message}`);
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
223
335
|
const [act, uidStr, accId] = String(data || '').split(':');
|
|
224
336
|
const u = getUserById(Number(uidStr));
|
|
225
337
|
if (!u || !accId) return;
|
|
@@ -265,12 +377,12 @@ async function pollTelegram() {
|
|
|
265
377
|
}
|
|
266
378
|
const u = tryLinkByCode(txt, 'tg', chatId);
|
|
267
379
|
if (u) {
|
|
268
|
-
await sendTelegram(chatId, `✓ Telegram успешно привязан к аккаунту НейроАльфа (${u.email}). Сюда будут приходить сигналы и кнопки подтверждения
|
|
380
|
+
await sendTelegram(chatId, `✓ Telegram успешно привязан к аккаунту НейроАльфа (${u.email}). Сюда будут приходить сигналы и кнопки подтверждения заявок, а также вы можете писать сюда любые вопросы ИИ-Стратегу (например: «Прогноз», «Портфель», «Что по SBER?»).`);
|
|
269
381
|
continue;
|
|
270
382
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
383
|
+
setImmediate(() => {
|
|
384
|
+
handleMessengerChat(txt, 'tg', chatId, (msg, btns) => sendTelegram(chatId, msg, btns)).catch(() => {});
|
|
385
|
+
});
|
|
274
386
|
} else if (upd.callback_query?.data) {
|
|
275
387
|
const chatId = upd.callback_query.message?.chat?.id;
|
|
276
388
|
const fromId = upd.callback_query.from?.id;
|
|
@@ -294,9 +406,14 @@ async function processMaxUpdate(upd) {
|
|
|
294
406
|
}
|
|
295
407
|
const u = tryLinkByCode(txt, 'max', chatId);
|
|
296
408
|
if (u) {
|
|
297
|
-
await sendMax(chatId, `✓ Мессенджер MAX успешно привязан к аккаунту НейроАльфа (${u.email}). Сюда будут приходить сигналы и кнопки подтверждения
|
|
409
|
+
await sendMax(chatId, `✓ Мессенджер MAX успешно привязан к аккаунту НейроАльфа (${u.email}). Сюда будут приходить сигналы и кнопки подтверждения заявок, а также вы можете писать сюда любые вопросы ИИ-Стратегу (например: «Прогноз», «Портфель», «Что по SBER?»).`);
|
|
298
410
|
return;
|
|
299
411
|
}
|
|
412
|
+
if (!upd.callback?.payload) {
|
|
413
|
+
setImmediate(() => {
|
|
414
|
+
handleMessengerChat(txt, 'max', chatId, (msg, btns) => sendMax(chatId, msg, btns)).catch(() => {});
|
|
415
|
+
});
|
|
416
|
+
}
|
|
300
417
|
}
|
|
301
418
|
if (upd.callback?.payload) {
|
|
302
419
|
const cbChatId = upd.chat_id || upd.message?.recipient?.chat_id || upd.callback?.user?.user_id;
|
|
@@ -7,6 +7,7 @@ import { chatStream, activeModelInfo, isLlmEnabled } from '../llm/client.js';
|
|
|
7
7
|
import { hub } from '../hub.js';
|
|
8
8
|
import { getSettings } from '../settings.js';
|
|
9
9
|
import { brokersState } from '../brokers/index.js';
|
|
10
|
+
import { registerChatHandlers } from '../notify/index.js';
|
|
10
11
|
import {
|
|
11
12
|
currentConversation, newConversation, getConversation, setConversationTitle, listConversations,
|
|
12
13
|
addMessage, updateMessage, getMessage, getMessages,
|
|
@@ -563,6 +564,117 @@ export function stopAgent() {
|
|
|
563
564
|
emitState();
|
|
564
565
|
}
|
|
565
566
|
|
|
567
|
+
// Быстрые сокращения и команды из мессенджеров (Telegram / MAX)
|
|
568
|
+
function expandMessengerPrompt(raw = '') {
|
|
569
|
+
const s = String(raw || '').trim();
|
|
570
|
+
let newSession = false;
|
|
571
|
+
let body = s;
|
|
572
|
+
if (/^\/new(?:\s+|$)/i.test(body)) {
|
|
573
|
+
newSession = true;
|
|
574
|
+
body = body.replace(/^\/new\s*/i, '').trim() || 'Посмотри текущий прогноз и состояние моих брокерских счетов: что сейчас важно?';
|
|
575
|
+
} else if (/^(\/forecast|прогноз|радар)$/i.test(body)) {
|
|
576
|
+
newSession = true;
|
|
577
|
+
body = 'Посмотри текущий прогноз на ближайшие часы (model_forecast) и свежие новости (get_news): какие акции сейчас сильнее и слабее рынка и есть ли сигналы?';
|
|
578
|
+
} else if (/^(\/portfolio|\/status|портфель|счет|счёт|счета|баланс)$/i.test(body)) {
|
|
579
|
+
newSession = true;
|
|
580
|
+
body = 'Проверь состояние моих брокерских счетов прямо сейчас (get_brokers_status) и расскажи по позициям, свободным рублям, марже и активным заявкам.';
|
|
581
|
+
} else if (/^(\/today|сегодня|сделки)$/i.test(body)) {
|
|
582
|
+
newSession = true;
|
|
583
|
+
body = 'Посмотри операции и сделки на моём брокерском счёте за сегодня (get_broker_operations с period="today") и разбери финансовый результат.';
|
|
584
|
+
} else if (/^(\/news|новости)$/i.test(body)) {
|
|
585
|
+
newSession = true;
|
|
586
|
+
body = 'Посмотри свежие новости по рынку (get_news) и скажи, на какие бумаги они влияют.';
|
|
587
|
+
}
|
|
588
|
+
return { text: body, newSession };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function collectAssistantReply(convId, afterMsgId) {
|
|
592
|
+
const msgs = getMessages(convId, 200).filter((m) => m.id > afterMsgId);
|
|
593
|
+
const parts = [];
|
|
594
|
+
for (const m of msgs) {
|
|
595
|
+
if (m.role === 'assistant' && m.content && !m.data?.hidden) {
|
|
596
|
+
parts.push(m.content.trim());
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
const w = userState().waiting;
|
|
600
|
+
if (w) {
|
|
601
|
+
const wMsg = getMessage(w.msgId);
|
|
602
|
+
const prev = wMsg?.data?.tools?.[w.callId]?.preview;
|
|
603
|
+
if (prev) parts.push(`\n⏳ Подтвердите действие кнопкой ниже:\n${prev}`);
|
|
604
|
+
}
|
|
605
|
+
const replyText = parts.join('\n\n').trim() || 'Готово.';
|
|
606
|
+
return { text: replyText, waiting: w || null };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Запрос к Стратегу из Telegram / MAX с ожиданием полного ответа (и поддержкой всех инструментов)
|
|
610
|
+
export async function askFromMessenger(rawText, { channel = 'tg', forceNewSession = false } = {}) {
|
|
611
|
+
const { text, newSession: cmdNew } = expandMessengerPrompt(rawText);
|
|
612
|
+
if (!text) throw new Error('Пустое сообщение');
|
|
613
|
+
// Если в текущем диалоге последнее сообщение было больше 45 минут назад — начинаем свежую сессию в архиве
|
|
614
|
+
const curMsgs = getMessages(conv().id, 20);
|
|
615
|
+
const lastMsgTs = curMsgs.length ? (curMsgs[curMsgs.length - 1].ts || 0) : 0;
|
|
616
|
+
const staleConv = curMsgs.some((m) => m.kind === 'user') && (Date.now() - lastMsgTs > 45 * 60000);
|
|
617
|
+
const newSession = Boolean(forceNewSession || cmdNew || staleConv);
|
|
618
|
+
const tag = channel === 'max' ? '[MAX]' : '[Telegram]';
|
|
619
|
+
if (userState().run) {
|
|
620
|
+
stopAgent();
|
|
621
|
+
}
|
|
622
|
+
if (newSession) {
|
|
623
|
+
resetConversation(`${tag} ${deriveSessionTitle(text)}`);
|
|
624
|
+
}
|
|
625
|
+
if (userState().waiting) {
|
|
626
|
+
rejectPending('Пользователь написал новое сообщение из мессенджера.');
|
|
627
|
+
}
|
|
628
|
+
const cur = conv();
|
|
629
|
+
if (!cur.title) {
|
|
630
|
+
const autoTitle = `${tag} ${deriveSessionTitle(text)}`;
|
|
631
|
+
userState().conv = setConversationTitle(cur.id, autoTitle) || { ...cur, title: autoTitle };
|
|
632
|
+
}
|
|
633
|
+
const m = addMessage(conv().id, { role: 'user', kind: 'user', content: text, data: { source: channel } });
|
|
634
|
+
emitMsg(m);
|
|
635
|
+
try { exportConversationMarkdown(conv().id); } catch { /* ignore */ }
|
|
636
|
+
await runAgent('advisor');
|
|
637
|
+
return collectAssistantReply(conv().id, m.id);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export async function resolveActionAndWait(msgId, callId, decision, comment = '') {
|
|
641
|
+
const w = userState().waiting;
|
|
642
|
+
if (!w || w.msgId !== msgId || w.callId !== callId) throw new Error('Это действие уже не ожидает подтверждения');
|
|
643
|
+
if (userState().run) stopAgent();
|
|
644
|
+
const mode = 'advisor';
|
|
645
|
+
const beforeMsgs = getMessages(conv().id, 200);
|
|
646
|
+
const lastId = beforeMsgs.length ? beforeMsgs[beforeMsgs.length - 1].id : msgId;
|
|
647
|
+
if (decision === 'approve') {
|
|
648
|
+
userState().waiting = null;
|
|
649
|
+
setToolStatus(msgId, callId, { status: 'approved' });
|
|
650
|
+
} else {
|
|
651
|
+
const msg = getMessage(msgId);
|
|
652
|
+
userState().waiting = null;
|
|
653
|
+
const call = msg.tool_calls.find((c) => c.id === callId);
|
|
654
|
+
const tm = addMessage(conv().id, { role: 'tool', kind: 'tool', tool_call_id: callId, content: JSON.stringify({ отклонено: true, комментарий: comment || 'пользователь отклонил в мессенджере' }), data: { name: call?.function.name, summary: 'Отклонено', ok: false } });
|
|
655
|
+
emitMsg(tm);
|
|
656
|
+
setToolStatus(msgId, callId, { status: 'rejected' });
|
|
657
|
+
}
|
|
658
|
+
if (!isLlmEnabled()) throw new Error('ИИ не подключён');
|
|
659
|
+
const myConvId = conv().id;
|
|
660
|
+
const myRun = { ctrl: new AbortController(), mode, convId: myConvId };
|
|
661
|
+
userState().run = myRun;
|
|
662
|
+
emitState();
|
|
663
|
+
try {
|
|
664
|
+
const res = await processCalls(getMessage(msgId), mode);
|
|
665
|
+
if (res === 'done') await loop(mode);
|
|
666
|
+
} catch (e) {
|
|
667
|
+
if (conv().id === myConvId) {
|
|
668
|
+
emitMsg(addMessage(myConvId, { role: 'assistant', kind: 'assistant', content: `⚠️ Ошибка: ${e.message}` }));
|
|
669
|
+
}
|
|
670
|
+
} finally {
|
|
671
|
+
if (userState().run === myRun) userState().run = null;
|
|
672
|
+
try { exportConversationMarkdown(myConvId); } catch { /* ignore */ }
|
|
673
|
+
emitState();
|
|
674
|
+
}
|
|
675
|
+
return collectAssistantReply(myConvId, lastId);
|
|
676
|
+
}
|
|
677
|
+
|
|
566
678
|
export function setMode() {
|
|
567
679
|
kvSet('strategistMode', 'advisor');
|
|
568
680
|
emitState();
|
|
@@ -640,4 +752,6 @@ hub.on((e) => {
|
|
|
640
752
|
}
|
|
641
753
|
});
|
|
642
754
|
|
|
755
|
+
registerChatHandlers({ ask: askFromMessenger, resolve: resolveActionAndWait });
|
|
756
|
+
|
|
643
757
|
export { fmtTs, buildContext, systemPrompt };
|