bc0ed1892f
Новый модуль assistant-health.js (по образцу classroom-cleanup): каждые 15 мин
пингует каждого провайдера (pingLLM) → app_settings.assistant_health
{ id:{ok,at,error,ms,fails} }. Авто-понижение: если активный провайдер
не отвечает 2+ раза подряд, а есть здоровый рабочий запасной — автоматически
переключает assistant_active и пишет assistant_failover (баннер «health»).
schedule() из server.js (unref).
Админка: тумблер «Авто-проверка провайдеров», кнопка «Проверить сейчас»
(POST /admin/assistant/health → runHealth), цветной индикатор здоровья на
каждой карточке провайдера (зелёный/красный + время/ошибка в title).
keyless-шлюзы и провайдеры без ключа учтены.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
59 lines
3.7 KiB
JavaScript
59 lines
3.7 KiB
JavaScript
'use strict';
|
|
/* Авто-здоровье LLM-провайдеров Квантика: периодический пинг каждого провайдера
|
|
* (lightweight pingLLM) + авто-понижение активного, если он стабильно не отвечает,
|
|
* а есть здоровый запасной. Результат — в app_settings.assistant_health (JSON-карта
|
|
* { id: { ok, at, error, ms, fails } }). Авто-переключение пишет тот же
|
|
* assistant_failover, что показывает баннер в админке. Период — 15 мин (вкл. по
|
|
* умолчанию; app_settings.assistant_health_enabled='0' выключает). */
|
|
const db = require('./db/db');
|
|
const logger = require('./utils/logger');
|
|
|
|
function _get(k) { const r = db.prepare('SELECT value FROM app_settings WHERE key=?').get(k); return r && r.value != null ? r.value : null; }
|
|
function _set(k, v) { db.prepare('INSERT OR REPLACE INTO app_settings (key,value) VALUES (?,?)').run(k, v); }
|
|
function _providers() { try { return JSON.parse(_get('assistant_providers') || '[]') || []; } catch (e) { return []; } }
|
|
function _enabled() { return _get('assistant_health_enabled') !== '0'; }
|
|
function _noKey(u) { return /\/\/(localhost|127\.0\.0\.1)/.test(u || '') || /\/\/[^/]*\bpollinations\.ai\b/i.test(u || ''); }
|
|
|
|
async function runHealthCheck() {
|
|
if (!_enabled()) return { skipped: true };
|
|
const provs = _providers();
|
|
if (!provs.length) return { providers: 0 };
|
|
const a = require('./controllers/assistantController');
|
|
let prev = {}; try { prev = JSON.parse(_get('assistant_health') || '{}') || {}; } catch (e) {}
|
|
const health = {};
|
|
for (const p of provs) {
|
|
// нет ключа и не keyless-шлюз — не пингуем (в FAQ-режиме), помечаем как «нет ключа»
|
|
if (!p.key && !_noKey(p.url)) { health[p.id] = { ok: false, at: new Date().toISOString(), error: 'нет ключа', ms: 0, fails: (prev[p.id] && prev[p.id].fails || 0) }; continue; }
|
|
const t0 = Date.now();
|
|
let r; try { r = await a.pingLLM({ url: p.url, model: p.model, key: p.key }); } catch (e) { r = { ok: false, error: 'сбой' }; }
|
|
const ok = !!(r && r.ok);
|
|
health[p.id] = {
|
|
ok, at: new Date().toISOString(), ms: Date.now() - t0,
|
|
error: ok ? null : String((r && (r.error || ('HTTP ' + r.status))) || 'ошибка').slice(0, 140),
|
|
fails: ok ? 0 : ((prev[p.id] && prev[p.id].fails || 0) + 1),
|
|
};
|
|
}
|
|
_set('assistant_health', JSON.stringify(health));
|
|
|
|
// авто-понижение активного: 2+ подряд неудач И есть здоровый рабочий запасной
|
|
const activeId = _get('assistant_active');
|
|
const active = provs.find(p => p.id === activeId);
|
|
if (active && health[activeId] && !health[activeId].ok && health[activeId].fails >= 2) {
|
|
const healthy = provs.find(p => p.id !== activeId && health[p.id] && health[p.id].ok && (p.key || _noKey(p.url)));
|
|
if (healthy) {
|
|
_set('assistant_active', healthy.id);
|
|
_set('assistant_failover', JSON.stringify({ at: new Date().toISOString(), failedId: activeId, failedName: active.name, servedId: healthy.id, servedName: healthy.name, reason: 'health', auto: true }));
|
|
logger.info('assistant-health auto-demote', { from: active.name, to: healthy.name, fails: health[activeId].fails });
|
|
}
|
|
}
|
|
return { providers: provs.length, health };
|
|
}
|
|
|
|
function schedule() {
|
|
const run = () => { runHealthCheck().catch(() => {}); };
|
|
setTimeout(run, 90_000).unref(); // первый прогон через 1.5 мин после старта
|
|
setInterval(run, 15 * 60 * 1000).unref(); // далее каждые 15 минут
|
|
}
|
|
|
|
module.exports = { runHealthCheck, schedule };
|