From ddc260e1144fbc5ec6ce2ba648601904cc889c28 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Fri, 12 Jun 2026 12:00:05 +0300 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=D1=82=D1=83=D0=BC=D0=B1=D0=BB?= =?UTF-8?q?=D0=B5=D1=80=20=D0=B2=D0=BA=D0=BB/=D0=B2=D1=8B=D0=BA=D0=BB=20?= =?UTF-8?q?=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BA?= =?UTF-8?q?=D0=B0=D1=80=D1=82=D0=B8=D0=BD=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Главный выключатель в разделе «Генерация картинок» (флаг on в конфиге, независим от наличия токена). Выключено → /api/imggen отдаёт 503 «временно выключена». Админ-тест работает и при выключенном тумблере (generateImage проверяет только наличие конфига). Бейдж различает «Включена / Выключена / Не настроена». Co-Authored-By: Claude Opus 4.8 --- backend/src/controllers/adminController.js | 7 ++++++- backend/src/controllers/imggenController.js | 10 +++++++--- frontend/js/admin/sections/imggen.js | 17 ++++++++++++++++- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/backend/src/controllers/adminController.js b/backend/src/controllers/adminController.js index 156625d..598b69c 100644 --- a/backend/src/controllers/adminController.js +++ b/backend/src/controllers/adminController.js @@ -1108,6 +1108,8 @@ function getImggen(_req, res) { let stats = { count: 0, bytes: 0 }; try { stats = require('./imggenController').stats(); } catch (e) {} const cd = Number(c.cooldownMs), dc = Number(c.dailyCap); + const configured = !!(c.provider === 'cloudflare' && c.accountId && c.token); + const on = c.on !== false; res.json({ provider: c.provider || 'cloudflare', accountId: c.accountId || '', @@ -1115,7 +1117,9 @@ function getImggen(_req, res) { hasToken: !!c.token, cooldownMs: Number.isFinite(cd) && cd >= 0 ? cd : 4000, dailyCap: Number.isFinite(dc) && dc >= 0 ? dc : 40, - enabled: !!(c.provider === 'cloudflare' && c.accountId && c.token), + on, + configured, + enabled: configured && on, models: IMGGEN_MODELS, stats, }); @@ -1125,6 +1129,7 @@ function saveImggen(req, res) { const b = req.body || {}; const c = _imgCfg(); c.provider = b.provider || c.provider || 'cloudflare'; + if (typeof b.on === 'boolean') c.on = b.on; if (b.accountId !== undefined) c.accountId = String(b.accountId || '').trim(); if (b.model !== undefined) c.model = String(b.model || '').trim(); if (b.clearToken) c.token = ''; diff --git a/backend/src/controllers/imggenController.js b/backend/src/controllers/imggenController.js index 372814c..7bd4d84 100644 --- a/backend/src/controllers/imggenController.js +++ b/backend/src/controllers/imggenController.js @@ -16,7 +16,9 @@ const DAILY_DEFAULT = 40; function _cfg() { try { const r = db.prepare("SELECT value FROM app_settings WHERE key='imggen_provider'").get(); return r ? JSON.parse(r.value) : null; } catch (e) { return null; } } -function _enabled() { const c = _cfg(); return !!(c && c.provider === 'cloudflare' && c.accountId && c.token); } +function _configured(c) { c = (c === undefined) ? _cfg() : c; return !!(c && c.provider === 'cloudflare' && c.accountId && c.token); } +// «Включено» = настроено И не выключено тумблером (c.on !== false). Тумблер независим от наличия токена. +function _enabled() { const c = _cfg(); return _configured(c) && c.on !== false; } function _limits() { const c = _cfg() || {}; const cd = Number(c.cooldownMs); @@ -95,7 +97,9 @@ function _save(uid, buf) { /* Публичная: перевод → генерация → сохранение. Без пер-юзер лимитов * (их применяет route generate). Возвращает { url, prompt } или бросает Error. */ async function generateImage(prompt, uid) { - if (!_enabled()) throw new Error('Генерация изображений не настроена'); + // Низкоуровневая: проверяем только наличие конфига (тумблер on/off — забота вызывающего + // route /api/imggen). Так админ-тест работает даже при выключенном тумблере. + if (!_configured()) throw new Error('Генерация изображений не настроена'); const p = String(prompt || '').trim().slice(0, 500); if (p.length < 3) throw new Error('Опиши, что нарисовать (хотя бы пару слов)'); const en = await _toEnglish(p); @@ -115,7 +119,7 @@ function stats() { /* POST /api/imggen { prompt } → { url } */ async function generate(req, res) { - if (!_enabled()) return res.status(503).json({ error: 'Генерация изображений не настроена' }); + if (!_enabled()) return res.status(503).json({ error: _configured() ? 'Генерация картинок временно выключена' : 'Генерация изображений не настроена' }); const prompt = String((req.body && req.body.prompt) || '').trim().slice(0, 500); if (prompt.length < 3) return res.status(400).json({ error: 'Опиши, что нарисовать (хотя бы пару слов)' }); diff --git a/frontend/js/admin/sections/imggen.js b/frontend/js/admin/sections/imggen.js index 90d167a..4266b13 100644 --- a/frontend/js/admin/sections/imggen.js +++ b/frontend/js/admin/sections/imggen.js @@ -42,12 +42,20 @@ try { cfg = await LS.api('/api/admin/imggen'); } catch (e) { host.innerHTML = '
Не удалось загрузить настройки
'; return; } var models = cfg.models || []; var cdSec = Math.round((cfg.cooldownMs != null ? cfg.cooldownMs : 4000) / 1000); + var on = cfg.on !== false; + var badgeTxt = cfg.enabled ? 'Включена' : (cfg.configured ? 'Выключена' : 'Не настроена'); + var badgeCls = cfg.enabled ? 'on' : 'off'; host.innerHTML = + '
' + + '
Генерация картинок включена
' + + '
Главный выключатель. Выключено — генерация недоступна во всей системе (ассистент, флэшкарты, уроки, питомец, обложки, аватар, доска), даже если токен задан.
' + + '' + + '
' + '
' + '
' + '
Cloudflare Workers AI
' + - '' + (cfg.enabled ? 'Включена' : 'Не настроена') + '' + + '' + badgeTxt + '' + '
' + '
Бесплатная генерация (FLUX.1 / SDXL). Токен и Account ID — из дашборда Cloudflare (Workers AI). Хранятся в БД, не в git.
' + @@ -84,6 +92,13 @@ var Q = function (s) { return host.querySelector(s); }; + Q('#img-master').addEventListener('change', function () { + var v = this.checked; + LS.api('/api/admin/imggen', { method: 'PUT', body: JSON.stringify({ on: v }) }) + .then(function () { LS.toast(v ? 'Генерация включена' : 'Генерация выключена для всех', 'success'); render(); }) + .catch(function () { LS.toast('Ошибка', 'error'); render(); }); + }); + Q('#img-save').addEventListener('click', async function () { var btn = this; btn.disabled = true; var body = {