From 78a9eca9c07f783162ac4c46eaf1e7e1593752e8 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Thu, 4 Jun 2026 21:06:31 +0300 Subject: [PATCH] =?UTF-8?q?fix(assistant):=20=D1=81=D0=BD=D1=8F=D1=82?= =?UTF-8?q?=D0=B8=D0=B5=20=D1=83=D1=81=D1=82=D0=B0=D1=80=D0=B5=D0=B2=D1=88?= =?UTF-8?q?=D0=B5=D0=B3=D0=BE=20=D1=84=D0=BB=D0=B0=D0=B3=D0=B0=20failover?= =?UTF-8?q?=20+=20=D1=87=D0=B8=D1=81=D1=82=D1=8B=D0=B9=20sample=20=D0=B2?= =?UTF-8?q?=20=D1=82=D0=B5=D1=81=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Баннер «провайдеры недоступны» висел из старой записи assistant_failover. Теперь успешный тест активного провайдера и смена активного снимают флаг, плюс кнопка «Снять» на баннере (PUT /assistant {dismissFailover}). Тест провайдера: system-инструкция + 64 токена + fallback на reasoning → sample не показывает «мысли вслух» reasoning-моделей. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/src/controllers/adminController.js | 4 ++++ backend/src/controllers/assistantController.js | 6 +++--- frontend/js/admin/sections/assistant.js | 12 ++++++++---- 3 files changed, 15 insertions(+), 7 deletions(-) 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 = '
' + bantxt + '
'; 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'); }); + }); } // ── Провайдеры (карточки) ──