diff --git a/backend/src/controllers/adminController.js b/backend/src/controllers/adminController.js
index bca0a53..6fec4ab 100644
--- a/backend/src/controllers/adminController.js
+++ b/backend/src/controllers/adminController.js
@@ -949,6 +949,7 @@ function saveAssistant(req, res) {
const b = req.body || {};
if (typeof b.rag === 'boolean') set('assistant_rag', b.rag ? '1' : '0');
if (typeof b.examButtons === 'boolean') set('assistant_exam_buttons', b.examButtons ? '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 });
}
@@ -985,6 +986,7 @@ function deleteProvider(req, res) {
function setActiveProvider(req, res) {
const id = String((req.body && req.body.id) || '');
db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('assistant_active', ?)").run(id);
+ try { db.prepare("DELETE FROM app_settings WHERE key = 'assistant_failover'").run(); } catch (e) {} // смена активного снимает старый флаг
audit(req, 'assistant.active', id, 'активный провайдер');
res.json({ ok: true });
}
@@ -1019,6 +1021,8 @@ async function testAssistant(req, res) {
override.local = _aIsLocal(override.url);
override.on = !!(override.key || override.local);
const r = await a.pingLLM(override);
+ // Успешный тест активного провайдера снимает устаревший флаг failover
+ try { const activeId = _aset('assistant_active'); if (r && r.ok && (!b.id || b.id === activeId)) a.clearFailover(); } catch (e) {}
res.json(r);
}
diff --git a/backend/src/controllers/assistantController.js b/backend/src/controllers/assistantController.js
index 40b4388..e9aba3f 100644
--- a/backend/src/controllers/assistantController.js
+++ b/backend/src/controllers/assistantController.js
@@ -355,7 +355,7 @@ async function pingLLM(override) {
const r = await fetch(cfg.url, {
method: 'POST',
headers: Object.assign({ 'Content-Type': 'application/json' }, cfg.key ? { Authorization: `Bearer ${cfg.key}` } : {}),
- body: JSON.stringify({ model: cfg.model, max_tokens: 16, messages: [{ role: 'user', content: 'Ответь одним словом: привет' }] }),
+ body: JSON.stringify({ model: cfg.model, max_tokens: 64, messages: [{ role: 'system', content: 'Отвечай сразу и кратко, без рассуждений вслух.' }, { role: 'user', content: 'Ответь одним словом: привет' }] }),
signal: ctrl.signal,
});
const txt = await r.text();
@@ -365,7 +365,7 @@ async function pingLLM(override) {
return { ok: false, status: r.status, error: msg };
}
let sample = '';
- try { const j = JSON.parse(txt); sample = String((j.choices && j.choices[0] && j.choices[0].message && j.choices[0].message.content) || '').slice(0, 120); } catch (e) {}
+ try { const j = JSON.parse(txt); const m = j.choices && j.choices[0] && j.choices[0].message; sample = String((m && (m.content || m.reasoning)) || '').replace(/\s+/g, ' ').trim().slice(0, 120); } catch (e) {}
return { ok: true, status: r.status, sample, model: cfg.model };
} catch (e) { return { ok: false, error: e.name === 'AbortError' ? 'Таймаут (15с)' : (e.message || 'Ошибка сети') }; } finally { clearTimeout(timer); }
}
@@ -502,4 +502,4 @@ async function flashcardsFromText(req, res) {
res.json({ title, cards });
}
-module.exports = { getContext, markSeen, dismiss, setSettings, ask, flashcardsFromText, feedback, llmConfig, pingLLM };
+module.exports = { getContext, markSeen, dismiss, setSettings, ask, flashcardsFromText, feedback, llmConfig, pingLLM, clearFailover: _clearFailover };
diff --git a/frontend/js/admin/sections/assistant.js b/frontend/js/admin/sections/assistant.js
index 1f00033..548b726 100644
--- a/frontend/js/admin/sections/assistant.js
+++ b/frontend/js/admin/sections/assistant.js
@@ -67,11 +67,15 @@
var fo = cfg.failover, rmap = { rate_limit: 'исчерпан лимит', http: 'ошибка API', timeout: 'таймаут', network: 'нет связи', error: 'ошибка' };
var when = ''; try { when = new Date(fo.at).toLocaleString('ru'); } catch (e) {}
var ban = document.createElement('div');
- ban.style.cssText = 'margin-top:14px;padding:11px 14px;border-radius:11px;background:rgba(245,158,11,.12);border:1px solid rgba(245,158,11,.4);color:#92400e;font-size:.84rem;line-height:1.5';
- ban.innerHTML = fo.servedName
- ? 'Переключение провайдера. «' + esc(fo.failedName || '?') + '» недоступен (' + (rmap[fo.reason] || 'ошибка') + ') — работаю на «' + esc(fo.servedName) + '». ' + (when ? '' + esc(when) + '' : '') + '
Снимется автоматически, когда активный снова заработает.'
- : 'Все провайдеры ИИ недоступны (' + (rmap[fo.reason] || 'ошибка') + '). Сейчас «Спроси» в FAQ-режиме.';
+ ban.style.cssText = 'margin-top:14px;padding:11px 14px;border-radius:11px;background:rgba(245,158,11,.12);border:1px solid rgba(245,158,11,.4);color:#92400e;font-size:.84rem;line-height:1.5;display:flex;align-items:flex-start;gap:12px';
+ var bantxt = fo.servedName
+ ? 'Переключение провайдера. «' + esc(fo.failedName || '?') + '» недоступен (' + (rmap[fo.reason] || 'ошибка') + ') — работаю на «' + esc(fo.servedName) + '». ' + (when ? '' + esc(when) + '' : '') + '
Снимется автоматически, когда активный снова заработает. Запись могла устареть — нажмите «Тест» на активном или снимите вручную.'
+ : 'Все провайдеры ИИ недоступны (' + (rmap[fo.reason] || 'ошибка') + '). Сейчас «Спроси» в FAQ-режиме. ' + (when ? '' + esc(when) + '' : '') + '
Если провайдер уже работает (тест проходит) — запись устарела, снимите её.';
+ ban.innerHTML = '