From 9f815e5143e6738ccbe63144038799dc1735d963 Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 6 Oct 2026 00:09:28 +0300 Subject: [PATCH] =?UTF-8?q?fix(shorts):=20=D0=B6=D0=B8=D0=B2=D1=8B=D0=B5?= =?UTF-8?q?=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=8B=20=D0=B8=20?= =?UTF-8?q?=D0=BB=D0=B8=D0=BC=D0=B8=D1=82=20=D0=BD=D0=B0=20=D0=B2=D1=8B?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D1=8B=20=D0=98=D0=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - shorts.js подписан на SSE shorts_changed с дебаунсом и реконнектом - dispatchEvent больше не отдаёт любой неизвестный тип как entries_changed: entries_changed/lessons_changed/shorts_changed теперь маршрутизируются по имени (иначе правка короткого сообщения будила журнал с ложным тостом о новой записи) - добавлен aiLimiter (30/мин на cache.rateLimitStore('ai')) и навешен на оба вызова модели: /api/ai/correct и /api/shorts/ai-correct --- public/js/shorts.js | 26 ++++++++++++++++++++++++++ server.js | 17 +++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/public/js/shorts.js b/public/js/shorts.js index cbc974a..0b06d7c 100644 --- a/public/js/shorts.js +++ b/public/js/shorts.js @@ -25,9 +25,35 @@ async function init() { document.getElementById('shortsSearch').addEventListener('keydown', e => { if (e.key === 'Enter') { shortsPage = 1; loadShorts(); } }); + connectShortsLive(); await loadShorts(); } +let shortsLiveSource = null; +let shortsRefreshTimer = null; + +function connectShortsLive() { + if (shortsLiveSource) { shortsLiveSource.close(); shortsLiveSource = null; } + const connect = () => { + const t = sessionStorage.getItem('authToken'); + if (!t) return; + shortsLiveSource = new EventSource(`${API}/api/events?token=${encodeURIComponent(t)}`); + shortsLiveSource.addEventListener('shorts_changed', () => { + if (shortsRefreshTimer) return; + shortsRefreshTimer = setTimeout(() => { + shortsRefreshTimer = null; + loadShorts(); + }, 300); + }); + shortsLiveSource.onerror = () => { + if (shortsLiveSource) { shortsLiveSource.close(); shortsLiveSource = null; } + setTimeout(connect, 5000); + }; + }; + connect(); + window.addEventListener('beforeunload', () => { if (shortsLiveSource) shortsLiveSource.close(); }); +} + let shortsPage = 1; const SHORTS_PAGE_SIZE = 50; let shortsTotal = 0; diff --git a/server.js b/server.js index 51a1a39..e0b74b4 100644 --- a/server.js +++ b/server.js @@ -159,6 +159,10 @@ function dispatchEvent(payload) { }); return; } + if (payload && (payload.type === 'entries_changed' || payload.type === 'lessons_changed' || payload.type === 'shorts_changed')) { + writeFrame(payload.type, { ts: Date.now() }); + return; + } if (payload && payload.type === 'ai_status') { writeFrame('ai_status', { id: payload.id, @@ -1991,6 +1995,15 @@ const chatLimiter = rateLimit({ message: { error: 'Слишком много сообщений. Подождите минуту.' }, }); +const aiLimiter = rateLimit({ + windowMs: 60 * 1000, + max: 30, + standardHeaders: true, + legacyHeaders: false, + store: cache.rateLimitStore('ai', 60 * 1000), + message: { error: 'Слишком много запросов к ИИ. Подождите минуту.' }, +}); + async function chatThreadFor(user, threadId) { const id = parseInt(threadId, 10); if (!Number.isInteger(id) || id < 1) return { error: 400, message: 'Invalid thread' }; @@ -2747,7 +2760,7 @@ app.delete('/api/shorts/:id/permanent', requireAuth, requireAdmin, async (req, r } }); -app.post('/api/shorts/ai-correct', requireAuth, async (req, res) => { +app.post('/api/shorts/ai-correct', requireAuth, aiLimiter, async (req, res) => { try { const text = reqStr(req.body?.text, 5000); if (!text) return res.status(400).json({ error: 'Текст не указан' }); @@ -7486,7 +7499,7 @@ app.put('/api/entries/:id/unschedule', requireAuth, async (req, res) => { res.json({ ok: true }); }); -app.post('/api/ai/correct', requireAuth, async (req, res) => { +app.post('/api/ai/correct', requireAuth, aiLimiter, async (req, res) => { const text = reqStr(req.body?.text, 5000); if (!text) return res.status(400).json({ error: 'Текст не указан' }); try {