fix(assistant): снятие устаревшего флага failover + чистый sample в тесте

Баннер «провайдеры недоступны» висел из старой записи assistant_failover.
Теперь успешный тест активного провайдера и смена активного снимают флаг,
плюс кнопка «Снять» на баннере (PUT /assistant {dismissFailover}).
Тест провайдера: system-инструкция + 64 токена + fallback на reasoning →
sample не показывает «мысли вслух» reasoning-моделей.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-04 21:06:31 +03:00
parent f748e074fd
commit 78a9eca9c0
3 changed files with 15 additions and 7 deletions
@@ -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);
}
@@ -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 };
+8 -4
View File
@@ -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
? '<b>Переключение провайдера.</b> «' + esc(fo.failedName || '?') + '» недоступен (' + (rmap[fo.reason] || 'ошибка') + ') — работаю на «' + esc(fo.servedName) + '». ' + (when ? '<span style="opacity:.7">' + esc(when) + '</span>' : '') + '<br><span style="opacity:.8">Снимется автоматически, когда активный снова заработает.</span>'
: '<b>Все провайдеры ИИ недоступны</b> (' + (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
? '<b>Переключение провайдера.</b> «' + esc(fo.failedName || '?') + '» недоступен (' + (rmap[fo.reason] || 'ошибка') + ') — работаю на «' + esc(fo.servedName) + '». ' + (when ? '<span style="opacity:.7">' + esc(when) + '</span>' : '') + '<br><span style="opacity:.8">Снимется автоматически, когда активный снова заработает. Запись могла устареть — нажмите «Тест» на активном или снимите вручную.</span>'
: '<b>Все провайдеры ИИ недоступны</b> (' + (rmap[fo.reason] || 'ошибка') + '). Сейчас «Спроси» в FAQ-режиме. ' + (when ? '<span style="opacity:.7">' + esc(when) + '</span>' : '') + '<br><span style="opacity:.8">Если провайдер уже работает (тест проходит) — запись устарела, снимите её.</span>';
ban.innerHTML = '<div style="flex:1">' + bantxt + '</div><button id="asst-fo-dismiss" class="asst-ib" style="flex-shrink:0;background:#92400e;border-color:#92400e;color:#fff">Снять</button>';
host.appendChild(ban);
ban.querySelector('#asst-fo-dismiss').addEventListener('click', function () {
LS.adminSaveAssistant({ dismissFailover: true }).then(function () { LS.toast('Уведомление снято', 'success'); render(); }).catch(function () { LS.toast('Ошибка', 'error'); });
});
}
// ── Провайдеры (карточки) ──