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.
- package/README.md +14 -10
- package/bin/cli.js +79 -69
- 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
- package/package.json +4 -5
- package/public/activate.html +41 -19
- package/public/activate.js +81 -10
- package/public/app.js +300 -152
- package/public/index.html +4 -37
- package/server/api/routes.js +570 -906
- package/server/auth/index.js +576 -761
- package/server/brokers/index.js +1 -4
- package/server/config.js +73 -53
- package/server/index.js +186 -187
- package/server/llm/local-engine.js +843 -0
- package/server/llm/providers.js +45 -34
- package/server/model/dataset.js +2 -4
- package/server/model/forecast.js +1 -3
- package/server/news/index.js +2 -3
- package/server/notify/index.js +304 -466
- package/deploy/docs/01-central.md +0 -96
- package/deploy/docs/02-cloud-instances.md +0 -130
- package/deploy/docs/03-desktop-build.md +0 -92
- package/deploy/docs/04-client-guide.md +0 -101
- package/deploy/docs/05-operations.md +0 -109
- package/deploy/docs/06-env-reference.md +0 -91
- package/deploy/docs/07-stress-test.md +0 -253
- package/deploy/docs/pdf/01-central.pdf +0 -0
- package/deploy/docs/pdf/02-cloud-instances.pdf +0 -0
- package/deploy/docs/pdf/03-desktop-build.pdf +0 -0
- package/deploy/docs/pdf/04-client-guide.pdf +0 -0
- package/deploy/docs/pdf/05-operations.pdf +0 -0
- package/deploy/docs/pdf/06-env-reference.pdf +0 -0
- package/deploy/docs/pdf/07-stress-test.pdf +0 -0
- package/deploy/install.sh +0 -128
- package/deploy/k8s/instance.yaml +0 -147
- package/public/admin.html +0 -604
- package/public/landing.html +0 -1207
- package/public/legal.html +0 -134
- package/scripts/set-admin-password.mjs +0 -30
- package/server/billing/yookassa.js +0 -142
- package/server/instance/agent.js +0 -259
- package/server/instance/central.js +0 -514
- package/server/instance/identity.js +0 -79
- package/server/instance/provision.js +0 -166
- package/server/instance/signing.js +0 -71
- package/server/role.js +0 -44
package/server/llm/providers.js
CHANGED
|
@@ -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 || '
|
|
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
|
|
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
|
-
|
|
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 ? '
|
|
257
|
-
editable:
|
|
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
|
-
|
|
357
|
-
|
|
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);
|
package/server/model/dataset.js
CHANGED
|
@@ -295,10 +295,9 @@ export async function loadDataset({ interval = BAR_INTERVAL, fromTs, tillTs, use
|
|
|
295
295
|
killer.unref?.();
|
|
296
296
|
}),
|
|
297
297
|
]);
|
|
298
|
-
} catch
|
|
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
|
|
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('Нет данных индекса МосБиржи');
|
package/server/model/forecast.js
CHANGED
|
@@ -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((
|
|
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);
|
package/server/news/index.js
CHANGED
|
@@ -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')
|
|
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
|
|
242
|
+
try { await collectOnce(); } catch { /* ошибки источников не выводятся в консоль */ } finally { busy = false; }
|
|
244
243
|
}
|
|
245
244
|
schedule(isTradingTime() ? 10 * 60000 : 30 * 60000);
|
|
246
245
|
}, ms);
|