neuroalpha 0.6.12 → 0.7.0

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.
Files changed (46) hide show
  1. package/README.md +14 -10
  2. package/bin/cli.js +79 -69
  3. package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +2 -2
  4. package/package.json +4 -5
  5. package/public/activate.html +41 -19
  6. package/public/activate.js +81 -10
  7. package/public/app.js +300 -152
  8. package/public/index.html +4 -37
  9. package/server/api/routes.js +570 -906
  10. package/server/auth/index.js +576 -761
  11. package/server/brokers/index.js +1 -4
  12. package/server/config.js +73 -53
  13. package/server/index.js +186 -187
  14. package/server/llm/local-engine.js +843 -0
  15. package/server/llm/providers.js +45 -34
  16. package/server/model/dataset.js +2 -4
  17. package/server/model/forecast.js +1 -3
  18. package/server/news/index.js +2 -3
  19. package/server/notify/index.js +304 -466
  20. package/deploy/docs/01-central.md +0 -96
  21. package/deploy/docs/02-cloud-instances.md +0 -130
  22. package/deploy/docs/03-desktop-build.md +0 -92
  23. package/deploy/docs/04-client-guide.md +0 -101
  24. package/deploy/docs/05-operations.md +0 -109
  25. package/deploy/docs/06-env-reference.md +0 -91
  26. package/deploy/docs/07-stress-test.md +0 -253
  27. package/deploy/docs/pdf/01-central.pdf +0 -0
  28. package/deploy/docs/pdf/02-cloud-instances.pdf +0 -0
  29. package/deploy/docs/pdf/03-desktop-build.pdf +0 -0
  30. package/deploy/docs/pdf/04-client-guide.pdf +0 -0
  31. package/deploy/docs/pdf/05-operations.pdf +0 -0
  32. package/deploy/docs/pdf/06-env-reference.pdf +0 -0
  33. package/deploy/docs/pdf/07-stress-test.pdf +0 -0
  34. package/deploy/install.sh +0 -128
  35. package/deploy/k8s/instance.yaml +0 -147
  36. package/public/admin.html +0 -604
  37. package/public/landing.html +0 -1207
  38. package/public/legal.html +0 -134
  39. package/scripts/set-admin-password.mjs +0 -30
  40. package/server/billing/yookassa.js +0 -142
  41. package/server/instance/agent.js +0 -259
  42. package/server/instance/central.js +0 -514
  43. package/server/instance/identity.js +0 -79
  44. package/server/instance/provision.js +0 -166
  45. package/server/instance/signing.js +0 -71
  46. package/server/role.js +0 -44
@@ -13,6 +13,7 @@ import { chatStream } from './client.js';
13
13
 
14
14
  const PROTOCOLS = ['openai', 'anthropic', 'gigachat', 'ollama'];
15
15
  const CLOUD_LLM_ID = 'neuroalpha_cloud';
16
+ const LOCAL_LLM_ID = 'local_llama';
16
17
  const CLOUD_MODEL_ID = 'Qwen/Qwen3.8-27B-FP8';
17
18
  const CLOUD_MODEL_LABEL = 'Qwen3.8 27b';
18
19
  const DEFAULT_URL = {
@@ -43,13 +44,16 @@ function readSecret(ref) {
43
44
  export function getKey(p) {
44
45
  let key = decryptSecret(p.apiKey || '') || readSecret(p.keyFile);
45
46
  if (!key && p.protocol === 'gigachat') key = (process.env.GIGACHAT_KEY || '').trim();
46
- if (!key && p.id === CLOUD_LLM_ID) key = kvGet('cloudLicense')?.apiKey || process.env.NEUROALPHA_API_KEY || 'na_demo';
47
+ if (!key && p.id === CLOUD_LLM_ID) key = kvGet('cloudLicense')?.apiKey || process.env.NEUROALPHA_TOKEN || process.env.NEUROALPHA_API_KEY || '';
47
48
  return key;
48
49
  }
49
50
 
50
51
  // ---------- адреса ----------
51
52
  export function chatUrl(p) {
52
- const base = String(p.baseUrl || DEFAULT_URL[p.protocol] || '').trim().replace(/\/+$/, '');
53
+ const dynamicCloudBase = p.id === CLOUD_LLM_ID
54
+ ? `${String(kvGet('cloudLicense')?.cloudUrl || DEFAULT_CLOUD_URL).replace(/\/+$/, '')}/api/llm/proxy/v1/chat/completions`
55
+ : '';
56
+ const base = String(dynamicCloudBase || p.baseUrl || DEFAULT_URL[p.protocol] || '').trim().replace(/\/+$/, '');
53
57
  if (!base) return '';
54
58
  const tail = p.protocol === 'anthropic' ? '/messages' : '/chat/completions';
55
59
  if (base.endsWith(tail)) return base;
@@ -182,14 +186,43 @@ function init() {
182
186
  kvSet('gigaCaMigrated', true);
183
187
  }
184
188
  ensureEnv();
185
- if (process.env.CLOUD_MODE !== '1') ensureCloudDefault();
189
+ ensureCloudDefault();
186
190
  const list = kvGet('llmProviders', []);
187
191
  if (!kvGet('llmSelected') && list.length) {
188
- const hit = findEnv(list) || list.flatMap((p) => p.models.map((m) => ({ p, m }))).find(({ p }) => getKey(p) || p.protocol === 'ollama');
192
+ const hit = findEnv(list) || list.flatMap((p) => p.models.map((m) => ({ p, m }))).find(({ p }) => getKey(p) || p.protocol === 'ollama' || p.id === LOCAL_LLM_ID);
189
193
  if (hit) kvSet('llmSelected', { providerId: hit.p.id, model: hit.m.id });
190
194
  }
191
195
  }
192
196
 
197
+ export function registerLocalLlamaProvider({ port, modelId, label, backend = '' }) {
198
+ const list = load();
199
+ const baseUrl = `http://127.0.0.1:${port}/v1/chat/completions`;
200
+ const modelEntry = { id: modelId, label: `${label}${backend ? ` · ${backend.toUpperCase()}` : ''}`, tools: true };
201
+ const idx = list.findIndex((p) => p.id === LOCAL_LLM_ID);
202
+ if (idx >= 0) {
203
+ const prevModels = (list[idx].models || []).filter((m) => m.id !== modelId);
204
+ list[idx] = {
205
+ ...list[idx],
206
+ name: 'Локальный ИИ (на этом ПК)',
207
+ protocol: 'openai',
208
+ baseUrl,
209
+ models: [modelEntry, ...prevModels],
210
+ source: 'local',
211
+ };
212
+ } else {
213
+ list.unshift({
214
+ id: LOCAL_LLM_ID,
215
+ name: 'Локальный ИИ (на этом ПК)',
216
+ protocol: 'openai',
217
+ baseUrl,
218
+ models: [modelEntry],
219
+ source: 'local',
220
+ });
221
+ }
222
+ save(list);
223
+ kvSet('llmSelected', { providerId: LOCAL_LLM_ID, model: modelId });
224
+ }
225
+
193
226
  function ensureCloudDefault() {
194
227
  const list = kvGet('llmProviders', []);
195
228
  if (list.some((p) => p.id === CLOUD_LLM_ID)) return;
@@ -249,12 +282,13 @@ const cleanModels = (models) => {
249
282
  const view = (p) => {
250
283
  const key = getKey(p);
251
284
  const isCloud = p.id === CLOUD_LLM_ID;
285
+ const isLocal = p.id === LOCAL_LLM_ID || p.source === 'local';
252
286
  const hasLic = Boolean(kvGet('cloudLicense')?.apiKey);
253
287
  return {
254
- id: p.id, name: p.name, protocol: p.protocol, baseUrl: p.baseUrl || DEFAULT_URL[p.protocol] || '', authUrl: p.authUrl || '', scope: p.scope || '',
255
- caFile: p.caFile || '', models: p.models, source: p.source, hasKey: !!key, needsKey: p.protocol !== 'ollama' && !isCloud,
256
- keySource: isCloud ? (hasLic ? 'API-ключ подписки' : 'демо-доступ') : (p.apiKey ? 'введён вручную' : p.keyFile ? (p.keyFile.startsWith('env:') ? '.env' : 'файл') : ''),
257
- editable: true,
288
+ id: p.id, name: p.name, protocol: p.protocol, baseUrl: chatUrl(p) || p.baseUrl || DEFAULT_URL[p.protocol] || '', authUrl: p.authUrl || '', scope: p.scope || '',
289
+ caFile: p.caFile || '', models: p.models, source: p.source, hasKey: !!key, needsKey: p.protocol !== 'ollama' && !isLocal && (!isCloud || !hasLic),
290
+ keySource: isLocal ? 'локальный движок llama.cpp' : (isCloud ? (hasLic ? 'токен личного кабинета' : 'нужна активация токеном ЛК') : (p.apiKey ? 'введён вручную' : p.keyFile ? (p.keyFile.startsWith('env:') ? '.env' : 'файл') : '')),
291
+ editable: !isLocal,
258
292
  };
259
293
  };
260
294
 
@@ -353,35 +387,12 @@ export function resolveActive() {
353
387
  const protocol = llmConfig.provider === 'anthropic' ? 'anthropic' : 'openai';
354
388
  return toTarget({ id: 'env', name: '.env', protocol, baseUrl: llmConfig.baseUrl, keyFile: 'env:LLM' }, { id: llmConfig.model, label: llmConfig.model, tools: true, effort: llmConfig.reasoningEffort });
355
389
  }
356
- if (process.env.CLOUD_MODE !== '1') {
357
- const cp = find(CLOUD_LLM_ID);
358
- if (cp && cp.models[0]) return toTarget(cp, cp.models[0]);
359
- }
360
- return null;
361
- }
362
-
363
- // Истинная апстрим-модель на облачном сервере (для проксирования запросов от npx-клиентов, исключая петлю)
364
- export function resolveUpstreamForProxy() {
365
- const list = load().filter((p) => p.id !== CLOUD_LLM_ID);
366
- const sel = getSelected();
367
- const p = sel && list.find((x) => x.id === sel.providerId);
368
- const m = p?.models.find((x) => x.id === sel.model);
369
- if (p && m && isReady(toTarget(p, m))) return toTarget(p, m);
370
- if (llmConfig.provider !== 'none' && llmConfig.baseUrl && llmConfig.model) {
371
- const protocol = llmConfig.provider === 'anthropic' ? 'anthropic' : 'openai';
372
- const t = toTarget({ id: 'env', name: '.env', protocol, baseUrl: llmConfig.baseUrl, keyFile: 'env:LLM' }, { id: llmConfig.model, label: llmConfig.model, tools: true, effort: llmConfig.reasoningEffort });
373
- if (isReady(t)) return t;
374
- }
375
- for (const prov of list) {
376
- for (const mod of prov.models) {
377
- const t = toTarget(prov, mod);
378
- if (isReady(t)) return t;
379
- }
380
- }
390
+ const cp = find(CLOUD_LLM_ID);
391
+ if (cp && cp.models[0] && getKey(cp)) return toTarget(cp, cp.models[0]);
381
392
  return null;
382
393
  }
383
394
 
384
- export const isReady = (t) => !!t && !!t.url && !!t.model && (t.protocol === 'ollama' || !!getKey(t.provider));
395
+ export const isReady = (t) => !!t && !!t.url && !!t.model && (t.protocol === 'ollama' || t.provider?.id === LOCAL_LLM_ID || !!getKey(t.provider));
385
396
 
386
397
  export async function testProvider(id, model) {
387
398
  const p = find(id);
@@ -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);