feat(assistant): авто-здоровье провайдеров + ручная проверка (фича 4/6)

Новый модуль 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>
This commit is contained in:
Maxim Dolgolyov
2026-06-24 15:02:37 +03:00
parent 40c3152fe8
commit bc0ed1892f
6 changed files with 90 additions and 5 deletions
+13 -1
View File
@@ -1008,6 +1008,8 @@ function getAssistant(_req, res) {
providers, activeId, active,
rag: _aset('assistant_rag') !== '0', examButtons: _aset('assistant_exam_buttons') === '1',
memory: _aset('assistant_memory') !== '0', socratic: _aset('assistant_socratic') === '1',
healthEnabled: _aset('assistant_health_enabled') !== '0',
health: (() => { try { return JSON.parse(_aset('assistant_health') || '{}') || {}; } catch (e) { return {}; } })(),
chunks, usage, usage30, feedback, failover, presets: ASSISTANT_PRESETS,
kiloModels: _kiloModels(), kiloModelsCustom: !!_aset('assistant_kilo_models'),
});
@@ -1021,6 +1023,7 @@ function saveAssistant(req, res) {
if (typeof b.examButtons === 'boolean') set('assistant_exam_buttons', b.examButtons ? '1' : '0');
if (typeof b.memory === 'boolean') set('assistant_memory', b.memory ? '1' : '0');
if (typeof b.socratic === 'boolean') set('assistant_socratic', b.socratic ? '1' : '0');
if (typeof b.healthEnabled === 'boolean') set('assistant_health_enabled', b.healthEnabled ? '1' : '0');
if (b.dismissFailover) { try { db.prepare("DELETE FROM app_settings WHERE key = 'assistant_failover'").run(); } catch (e) {} }
audit(req, 'assistant.config', 'assistant', 'настройки');
res.json({ ok: true });
@@ -1210,6 +1213,15 @@ function applyModels(req, res) {
res.json({ ok: true, count: clean.length });
}
/* POST /api/admin/assistant/health — прогнать проверку здоровья провайдеров сейчас */
async function runHealth(req, res) {
try {
const r = await require('../assistant-health').runHealthCheck();
audit(req, 'assistant.health', 'assistant', 'ручная проверка');
res.json({ ok: true, result: r });
} catch (e) { res.status(500).json({ ok: false, error: e.message || 'ошибка' }); }
}
/* POST /api/admin/assistant/active { id } — выбрать активного провайдера */
function setActiveProvider(req, res) {
const id = String((req.body && req.body.id) || '');
@@ -1323,5 +1335,5 @@ module.exports = {
getTopics, createTopic, updateTopic, deleteTopic,
broadcast,
getAssistant, saveAssistant, testAssistant, reindexTextbooks, saveProvider, deleteProvider, setActiveProvider, getProviderModels,
scanModels, probeModel, applyModels,
scanModels, probeModel, applyModels, runHealth,
};