neuroalpha 0.6.12 → 0.6.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuroalpha",
3
- "version": "0.6.12",
3
+ "version": "0.6.13",
4
4
  "description": "НейроАльфа — ИИ-автопилот для рынка акций Мосбиржи: эмбеддинги акций, самообучение правил, коннекторы Т-Банк / БКС / Финам / Алор и боты Telegram / MAX",
5
5
  "author": "Roman A. Perepelkin",
6
6
  "license": "UNLICENSED",
@@ -191,7 +191,6 @@ function logBrokerEvent(acc, kind, text) {
191
191
  const uid = currentUserId();
192
192
  db.prepare('INSERT INTO broker_events (ts, user_id, account_id, kind, text) VALUES (?, ?, ?, ?, ?)')
193
193
  .run(Date.now(), uid, String(acc?.id || ''), String(kind), String(text || '').slice(0, 500));
194
- if (/error|stop|halt/.test(kind)) console.warn(`[брокер ${acc?.name || acc?.id}] ${kind}: ${text}`);
195
194
  // храним последние 5000 событий на счёт
196
195
  if (Math.random() < 0.02) {
197
196
  db.prepare(`DELETE FROM broker_events WHERE user_id = ? AND account_id = ? AND id NOT IN
@@ -275,7 +274,6 @@ function migrateKvAccounts(uid) {
275
274
  db.exec('COMMIT');
276
275
  } catch (e) { db.exec('ROLLBACK'); throw e; }
277
276
  // прежняя запись не удаляется: при откате на версию до 0.4.1 счета останутся на месте (в состоянии на момент переноса)
278
- console.log(`✓ Брокерские счета пользователя ${uid} перенесены в отдельные записи (${old.length})`);
279
277
  }
280
278
  userKvSet('brokerAccountsMigrated', true, uid);
281
279
  }
package/server/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // Copyright (c) 2026 Roman A. Perepelkin. All rights reserved. Proprietary software.
2
2
  import http from 'node:http';
3
+ import fs from 'node:fs';
3
4
  import os from 'node:os';
4
5
  import { spawn } from 'node:child_process';
5
6
  import path from 'node:path';
@@ -141,11 +142,26 @@ app.use('/docs', express.static(path.join(ROOT, 'docs')));
141
142
  app.use(express.static(path.join(ROOT, 'public')));
142
143
 
143
144
  function lanUrls() {
144
- return Object.values(os.networkInterfaces()).flat()
145
+ return Object.entries(os.networkInterfaces())
146
+ .filter(([name]) => !/vethernet|wsl|docker|hyper-v|virtualbox|vmware/i.test(name))
147
+ .flatMap(([, list]) => list || [])
145
148
  .filter((a) => a && a.family === 'IPv4' && !a.internal)
146
149
  .map((a) => `http://${a.address}:${PORT}`);
147
150
  }
148
151
 
152
+ function fmtClickableUrl(url, out = process.stdout) {
153
+ if (!out?.isTTY) return url;
154
+ const osc8 = Boolean(
155
+ process.env.WT_SESSION
156
+ || process.env.TERM_PROGRAM
157
+ || process.env.COLORTERM
158
+ || process.env.FORCE_HYPERLINK === '1'
159
+ || (process.platform !== 'win32' && process.env.TERM && process.env.TERM !== 'dumb'),
160
+ );
161
+ const styled = `\x1b[4;96m${url}\x1b[0m`;
162
+ return osc8 ? `\x1b]8;;${url}\x1b\\${styled}\x1b]8;;\x1b\\` : styled;
163
+ }
164
+
149
165
  const shouldOpenBrowser = process.env.OPEN_BROWSER === '1'
150
166
  || (process.env.OPEN_BROWSER !== '0'
151
167
  && process.env.NO_BROWSER !== '1'
@@ -157,9 +173,10 @@ const shouldOpenBrowser = process.env.OPEN_BROWSER === '1'
157
173
 
158
174
  wss.on('error', () => {});
159
175
  server.on('error', (e) => {
176
+ const clientUrl = `http://localhost:${PORT}`;
160
177
  if (e.code === 'EADDRINUSE') {
161
- console.error(`Порт ${PORT} занят: сервер уже запущен? Клиент: http://localhost:${PORT}`);
162
- if (shouldOpenBrowser) openBrowser(`http://localhost:${PORT}`);
178
+ console.error(`Порт ${PORT} занят: сервер уже запущен? Клиент: ${fmtClickableUrl(clientUrl, process.stderr)}`);
179
+ if (shouldOpenBrowser) openBrowser(clientUrl);
163
180
  } else {
164
181
  console.error(e);
165
182
  }
@@ -168,12 +185,20 @@ server.on('error', (e) => {
168
185
 
169
186
  server.listen(PORT, LISTEN_HOST, () => {
170
187
  const ident = IS_INSTANCE ? getIdentity() : null;
171
- console.log(`Консорциум агентов запущен · режим ${ROLE}${IS_INSTANCE ? ` (${INSTANCE_KIND}) · машина ${ident.id || 'не активирована'} · центр ${ident.centralUrl}` : ''}`);
188
+ let ver = '';
189
+ try { ver = ` v${JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version}`; } catch { /* ignore */ }
190
+ console.log(`◈ НейроАльфа${ver} запущена${IS_INSTANCE ? ` (${INSTANCE_KIND}) · машина ${ident.id || 'не активирована'} · центр ${ident.centralUrl}` : (IS_CENTRAL ? ' · центр' : '')}`);
172
191
  const llm = activeModelInfo();
173
- console.log(`ИИ-стратег: ${llm.enabled ? `${llm.provider} · ${llm.model}` : 'выключен (нет модели или ключа: Настройки → Модели ИИ)'}`);
174
- if (!LISTEN_HOST || LISTEN_HOST === '0.0.0.0') for (const url of lanUrls()) console.log(` В локальной сети: ${url}`);
175
- console.log(`Клиент: http://localhost:${PORT}`);
176
- if (shouldOpenBrowser) openBrowser(`http://localhost:${PORT}`);
192
+ console.log(` ИИ-стратег: ${llm.enabled ? `${llm.provider} · ${llm.model}` : 'выключен (Настройки → Модели ИИ)'}`);
193
+ if (!LISTEN_HOST || LISTEN_HOST === '0.0.0.0') for (const url of lanUrls()) console.log(` В сети: ${fmtClickableUrl(url)}`);
194
+ const clientUrl = `http://localhost:${PORT}`;
195
+ const enterHint = process.stdin?.isTTY && !IS_CENTRAL ? ' (Enter — открыть в браузере)' : '';
196
+ console.log(` Клиент: ${fmtClickableUrl(clientUrl)}${enterHint}`);
197
+ if (shouldOpenBrowser) openBrowser(clientUrl);
198
+ if (process.stdin?.isTTY && !IS_CENTRAL) {
199
+ process.stdin.on('data', () => openBrowser(clientUrl));
200
+ process.stdin.unref?.();
201
+ }
177
202
  });
178
203
 
179
204
  function openBrowser(url) {
@@ -295,10 +295,9 @@ export async function loadDataset({ interval = BAR_INTERVAL, fromTs, tillTs, use
295
295
  killer.unref?.();
296
296
  }),
297
297
  ]);
298
- } catch (e) {
298
+ } catch {
299
299
  if (signal?.aborted) throw new Error('Отменено');
300
300
  raw[s] = EMPTY();
301
- console.warn(`[dataset] ${s}: ${e.message} — акция пропущена`);
302
301
  } finally {
303
302
  clearTimeout(killer);
304
303
  onProgress({ symbol: s, done: ++done, total: all.length });
@@ -309,10 +308,9 @@ export async function loadDataset({ interval = BAR_INTERVAL, fromTs, tillTs, use
309
308
  try {
310
309
  onProgress({ symbol: 'BR', done: done, total: all.length });
311
310
  raw.BR = await loadBrent(fromTs, tillTs, signal, { localOnly });
312
- } catch (e) {
311
+ } catch {
313
312
  if (signal?.aborted) throw new Error('Отменено');
314
313
  raw.BR = EMPTY();
315
- console.warn(`[dataset] BR: ${e.message} — нефть пропущена`);
316
314
  }
317
315
  }
318
316
  if (!raw.IMOEX?.n) throw new Error('Нет данных индекса МосБиржи');
@@ -966,9 +966,7 @@ export async function forecastNow(input, { force = false, onProgress } = {}) {
966
966
  if (saved?.result && saved.key === key && now - (saved.result.at || 0) < 4 * 3600000) {
967
967
  if (!inflight.has(key)) {
968
968
  setImmediate(() => {
969
- refreshDatasetStateInBackground(p, key, { force: false }).catch((err) => {
970
- console.warn('[forecast] background warm-up error:', err?.message || err);
971
- });
969
+ refreshDatasetStateInBackground(p, key, { force: false }).catch(() => {});
972
970
  });
973
971
  }
974
972
  const liveSnap = await fetchLiveMoexSnapshot().catch(() => null);
@@ -146,7 +146,7 @@ async function collectOnce() {
146
146
  const perSource = {};
147
147
  let fresh = 0;
148
148
  results.forEach((r, i) => {
149
- if (r.status !== 'fulfilled') { console.error(`Новости: ${SOURCES[i].id}: ${r.reason?.message || r.reason}`); return; }
149
+ if (r.status !== 'fulfilled') return;
150
150
  let n = 0;
151
151
  for (const it of r.value) {
152
152
  if (it.ts < Date.now() - 3 * DAY || it.ts > Date.now() + 3600000) continue; // старьё и будущее не берём
@@ -157,7 +157,6 @@ async function collectOnce() {
157
157
  });
158
158
  fresh = Object.values(perSource).reduce((a, b) => a + b, 0);
159
159
  if (fresh) {
160
- console.log(`Новости: +${fresh} (${Object.entries(perSource).map(([k, v]) => `${k} ${v}`).join(', ')})`);
161
160
  hub.emit({ type: 'news:update', fresh });
162
161
  }
163
162
  await markPending(vocab, m);
@@ -240,7 +239,7 @@ function schedule(ms) {
240
239
  timer = setTimeout(async () => {
241
240
  if (!busy) {
242
241
  busy = true;
243
- try { await collectOnce(); } catch (e) { console.error('Новости:', e.message); } finally { busy = false; }
242
+ try { await collectOnce(); } catch { /* ошибки источников не выводятся в консоль */ } finally { busy = false; }
244
243
  }
245
244
  schedule(isTradingTime() ? 10 * 60000 : 30 * 60000);
246
245
  }, ms);