feat(admin): тумблер вкл/выкл генерации картинок

Главный выключатель в разделе «Генерация картинок» (флаг on в конфиге,
независим от наличия токена). Выключено → /api/imggen отдаёт 503
«временно выключена». Админ-тест работает и при выключенном тумблере
(generateImage проверяет только наличие конфига). Бейдж различает
«Включена / Выключена / Не настроена».

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-12 12:00:05 +03:00
parent 88651d85ab
commit ddc260e114
3 changed files with 29 additions and 5 deletions
+6 -1
View File
@@ -1108,6 +1108,8 @@ function getImggen(_req, res) {
let stats = { count: 0, bytes: 0 }; let stats = { count: 0, bytes: 0 };
try { stats = require('./imggenController').stats(); } catch (e) {} try { stats = require('./imggenController').stats(); } catch (e) {}
const cd = Number(c.cooldownMs), dc = Number(c.dailyCap); 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({ res.json({
provider: c.provider || 'cloudflare', provider: c.provider || 'cloudflare',
accountId: c.accountId || '', accountId: c.accountId || '',
@@ -1115,7 +1117,9 @@ function getImggen(_req, res) {
hasToken: !!c.token, hasToken: !!c.token,
cooldownMs: Number.isFinite(cd) && cd >= 0 ? cd : 4000, cooldownMs: Number.isFinite(cd) && cd >= 0 ? cd : 4000,
dailyCap: Number.isFinite(dc) && dc >= 0 ? dc : 40, dailyCap: Number.isFinite(dc) && dc >= 0 ? dc : 40,
enabled: !!(c.provider === 'cloudflare' && c.accountId && c.token), on,
configured,
enabled: configured && on,
models: IMGGEN_MODELS, models: IMGGEN_MODELS,
stats, stats,
}); });
@@ -1125,6 +1129,7 @@ function saveImggen(req, res) {
const b = req.body || {}; const b = req.body || {};
const c = _imgCfg(); const c = _imgCfg();
c.provider = b.provider || c.provider || 'cloudflare'; 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.accountId !== undefined) c.accountId = String(b.accountId || '').trim();
if (b.model !== undefined) c.model = String(b.model || '').trim(); if (b.model !== undefined) c.model = String(b.model || '').trim();
if (b.clearToken) c.token = ''; if (b.clearToken) c.token = '';
+7 -3
View File
@@ -16,7 +16,9 @@ const DAILY_DEFAULT = 40;
function _cfg() { 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; } 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() { function _limits() {
const c = _cfg() || {}; const c = _cfg() || {};
const cd = Number(c.cooldownMs); const cd = Number(c.cooldownMs);
@@ -95,7 +97,9 @@ function _save(uid, buf) {
/* Публичная: перевод → генерация → сохранение. Без пер-юзер лимитов /* Публичная: перевод → генерация → сохранение. Без пер-юзер лимитов
* (их применяет route generate). Возвращает { url, prompt } или бросает Error. */ * (их применяет route generate). Возвращает { url, prompt } или бросает Error. */
async function generateImage(prompt, uid) { 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); const p = String(prompt || '').trim().slice(0, 500);
if (p.length < 3) throw new Error('Опиши, что нарисовать (хотя бы пару слов)'); if (p.length < 3) throw new Error('Опиши, что нарисовать (хотя бы пару слов)');
const en = await _toEnglish(p); const en = await _toEnglish(p);
@@ -115,7 +119,7 @@ function stats() {
/* POST /api/imggen { prompt } → { url } */ /* POST /api/imggen { prompt } → { url } */
async function generate(req, res) { 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); const prompt = String((req.body && req.body.prompt) || '').trim().slice(0, 500);
if (prompt.length < 3) return res.status(400).json({ error: 'Опиши, что нарисовать (хотя бы пару слов)' }); if (prompt.length < 3) return res.status(400).json({ error: 'Опиши, что нарисовать (хотя бы пару слов)' });
+16 -1
View File
@@ -42,12 +42,20 @@
try { cfg = await LS.api('/api/admin/imggen'); } catch (e) { host.innerHTML = '<div style="color:#e0335e">Не удалось загрузить настройки</div>'; return; } try { cfg = await LS.api('/api/admin/imggen'); } catch (e) { host.innerHTML = '<div style="color:#e0335e">Не удалось загрузить настройки</div>'; return; }
var models = cfg.models || []; var models = cfg.models || [];
var cdSec = Math.round((cfg.cooldownMs != null ? cfg.cooldownMs : 4000) / 1000); 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 = host.innerHTML =
'<div class="perm-card' + (on ? ' enabled' : '') + '">' +
'<div class="perm-info"><div class="perm-label">Генерация картинок включена</div>' +
'<div class="perm-desc">Главный выключатель. Выключено — генерация недоступна во всей системе (ассистент, флэшкарты, уроки, питомец, обложки, аватар, доска), даже если токен задан.</div></div>' +
'<label class="perm-toggle"><input type="checkbox" id="img-master" ' + (on ? 'checked' : '') + '><span class="perm-track"></span><span class="perm-thumb"></span></label>' +
'</div>' +
'<div class="img-card">' + '<div class="img-card">' +
'<div style="display:flex;align-items:center;justify-content:space-between;gap:10px">' + '<div style="display:flex;align-items:center;justify-content:space-between;gap:10px">' +
'<div style="font-weight:800;font-size:.95rem">Cloudflare Workers AI</div>' + '<div style="font-weight:800;font-size:.95rem">Cloudflare Workers AI</div>' +
'<span class="img-bdg ' + (cfg.enabled ? 'on' : 'off') + '">' + (cfg.enabled ? 'Включена' : 'Не настроена') + '</span>' + '<span class="img-bdg ' + badgeCls + '">' + badgeTxt + '</span>' +
'</div>' + '</div>' +
'<div class="img-hint">Бесплатная генерация (FLUX.1 / SDXL). Токен и Account ID — из дашборда Cloudflare (Workers AI). Хранятся в БД, не в git.</div>' + '<div class="img-hint">Бесплатная генерация (FLUX.1 / SDXL). Токен и Account ID — из дашборда Cloudflare (Workers AI). Хранятся в БД, не в git.</div>' +
@@ -84,6 +92,13 @@
var Q = function (s) { return host.querySelector(s); }; 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 () { Q('#img-save').addEventListener('click', async function () {
var btn = this; btn.disabled = true; var btn = this; btn.disabled = true;
var body = { var body = {