diff --git a/backend/src/controllers/adminController.js b/backend/src/controllers/adminController.js index 406aecc..bc9a54b 100644 --- a/backend/src/controllers/adminController.js +++ b/backend/src/controllers/adminController.js @@ -940,6 +940,7 @@ const ASSISTANT_PRESETS = [ { name: 'Groq', url: 'https://api.groq.com/openai/v1/chat/completions', model: 'llama-3.3-70b-versatile' }, { name: 'OpenRouter', url: 'https://openrouter.ai/api/v1/chat/completions', model: 'meta-llama/llama-3.3-70b-instruct:free' }, { name: 'HuggingFace Router', url: 'https://router.huggingface.co/v1/chat/completions', model: 'Qwen/Qwen2.5-72B-Instruct' }, + { name: 'Pollinations (без ключа)', url: 'https://text.pollinations.ai/openai', model: 'openai' }, { name: 'Ollama (локально)', url: 'http://localhost:11434/v1/chat/completions', model: 'qwen2.5:3b' }, ]; // Проверенные бесплатные модели шлюза Kilo (отдают чистый русский). Порядок — от мощных к лёгким. @@ -967,6 +968,8 @@ function _kiloModels() { function _aProviders() { try { return JSON.parse(_aset('assistant_providers') || '[]') || []; } catch (e) { return []; } } function _aSetProviders(arr) { db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('assistant_providers', ?)").run(JSON.stringify(arr)); } function _aIsLocal(u) { return /\/\/(localhost|127\.0\.0\.1)/.test(u || ''); } +// Шлюзы с бесплатным инференсом без ключа (как localhost): ключ не обязателен. +function _aNoKey(u) { return _aIsLocal(u) || /\/\/[^/]*\bpollinations\.ai\b/i.test(u || ''); } function getAssistant(_req, res) { // Миграция legacy-настроек в список провайдеров (один раз) @@ -980,10 +983,10 @@ function getAssistant(_req, res) { } } const list = _aProviders(); - const providers = list.map(p => ({ id: p.id, name: p.name, url: p.url, model: p.model, hasKey: !!p.key, ctx: p.ctx || null, out: p.out || null, free: (typeof p.free === 'boolean' ? p.free : null) })); + const providers = list.map(p => ({ id: p.id, name: p.name, url: p.url, model: p.model, hasKey: !!p.key, noKey: _aNoKey(p.url), ctx: p.ctx || null, out: p.out || null, free: (typeof p.free === 'boolean' ? p.free : null) })); const activeId = _aset('assistant_active') || (providers[0] && providers[0].id) || null; const ap = list.find(p => p.id === activeId); - const active = !!(ap && (ap.key || _aIsLocal(ap.url))); + const active = !!(ap && (ap.key || _aNoKey(ap.url))); let chunks = 0, usage = { model_calls: 0, cache_hits: 0, faq: 0 }, usage30 = { model_calls: 0, cache_hits: 0, faq: 0 }; try { chunks = db.prepare('SELECT COUNT(*) n FROM textbook_chunks').get().n; } catch (e) {} @@ -1243,7 +1246,7 @@ async function testAssistant(req, res) { }; } override.local = _aIsLocal(override.url); - override.on = !!(override.key || override.local); + override.on = !!(override.key || _aNoKey(override.url)); 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) {} diff --git a/backend/src/controllers/assistantController.js b/backend/src/controllers/assistantController.js index 3e684ad..a31f897 100644 --- a/backend/src/controllers/assistantController.js +++ b/backend/src/controllers/assistantController.js @@ -339,6 +339,8 @@ function searchFaq(q, n) { * на ENV и дефолты. Если ключа нет и URL не локальный — работаем как FAQ. */ function _setting(k) { try { const r = db.prepare('SELECT value FROM app_settings WHERE key = ?').get(k); return r && r.value != null ? r.value : null; } catch (e) { return null; } } function _isLocal(url) { return /\/\/(localhost|127\.0\.0\.1)/.test(url || ''); } +// Шлюзы с бесплатным инференсом БЕЗ ключа (наряду с localhost): ключ не обязателен. +function _noKeyNeeded(url) { return _isLocal(url) || /\/\/[^/]*\bpollinations\.ai\b/i.test(url || ''); } /* Список провайдеров (несколько ключей/моделей). Хранится JSON в app_settings. * Если списка нет — синтезируем из legacy-настроек/ENV, чтобы ничего не сломать. */ @@ -357,7 +359,7 @@ function _providers() { /* Конфиги в порядке использования: активный первым, затем остальные с ключом * (для авто-перехвата при лимите/ошибке). */ function providersOrdered() { - const arr = _providers().filter(p => p && (p.key || _isLocal(p.url))); + const arr = _providers().filter(p => p && (p.key || _noKeyNeeded(p.url))); const activeId = _setting('assistant_active'); const active = arr.filter(p => p.id === activeId); const rest = arr.filter(p => p.id !== activeId); @@ -455,7 +457,7 @@ async function callLLMFailover(messages, maxTokens) { async function pingLLM(override) { const cfg = override || llmConfig(); if (!cfg.url) return { ok: false, error: 'URL не задан' }; - if (!cfg.key && !/\/\/(localhost|127\.0\.0\.1)/.test(cfg.url)) return { ok: false, error: 'Ключ не задан' }; + if (!cfg.key && !_noKeyNeeded(cfg.url)) return { ok: false, error: 'Ключ не задан' }; if (typeof fetch !== 'function') return { ok: false, error: 'fetch недоступен' }; const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 15000); diff --git a/frontend/js/admin/sections/assistant.js b/frontend/js/admin/sections/assistant.js index 106da44..4cab1e5 100644 --- a/frontend/js/admin/sections/assistant.js +++ b/frontend/js/admin/sections/assistant.js @@ -161,7 +161,7 @@ '