fix(shorts): живые обновления страницы и лимит на вызовы ИИ
- 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
This commit is contained in:
@@ -25,9 +25,35 @@ async function init() {
|
|||||||
document.getElementById('shortsSearch').addEventListener('keydown', e => {
|
document.getElementById('shortsSearch').addEventListener('keydown', e => {
|
||||||
if (e.key === 'Enter') { shortsPage = 1; loadShorts(); }
|
if (e.key === 'Enter') { shortsPage = 1; loadShorts(); }
|
||||||
});
|
});
|
||||||
|
connectShortsLive();
|
||||||
await loadShorts();
|
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;
|
let shortsPage = 1;
|
||||||
const SHORTS_PAGE_SIZE = 50;
|
const SHORTS_PAGE_SIZE = 50;
|
||||||
let shortsTotal = 0;
|
let shortsTotal = 0;
|
||||||
|
|||||||
@@ -159,6 +159,10 @@ function dispatchEvent(payload) {
|
|||||||
});
|
});
|
||||||
return;
|
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') {
|
if (payload && payload.type === 'ai_status') {
|
||||||
writeFrame('ai_status', {
|
writeFrame('ai_status', {
|
||||||
id: payload.id,
|
id: payload.id,
|
||||||
@@ -1991,6 +1995,15 @@ const chatLimiter = rateLimit({
|
|||||||
message: { error: 'Слишком много сообщений. Подождите минуту.' },
|
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) {
|
async function chatThreadFor(user, threadId) {
|
||||||
const id = parseInt(threadId, 10);
|
const id = parseInt(threadId, 10);
|
||||||
if (!Number.isInteger(id) || id < 1) return { error: 400, message: 'Invalid thread' };
|
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 {
|
try {
|
||||||
const text = reqStr(req.body?.text, 5000);
|
const text = reqStr(req.body?.text, 5000);
|
||||||
if (!text) return res.status(400).json({ error: 'Текст не указан' });
|
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 });
|
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);
|
const text = reqStr(req.body?.text, 5000);
|
||||||
if (!text) return res.status(400).json({ error: 'Текст не указан' });
|
if (!text) return res.status(400).json({ error: 'Текст не указан' });
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user