187 lines
6.6 KiB
JavaScript
187 lines
6.6 KiB
JavaScript
const express = require('express');
|
||
const router = express.Router();
|
||
const db = require('../lib/db');
|
||
|
||
// Public API — used by landing and dashboard to load dynamic content
|
||
|
||
// Get all settings as key-value object
|
||
router.get('/settings', async (req, res) => {
|
||
try {
|
||
const [rows] = await db.pool.execute('SELECT setting_key, setting_value FROM settings');
|
||
const settings = {};
|
||
for (const r of rows) settings[r.setting_key] = r.setting_value;
|
||
res.json(settings);
|
||
} catch (err) {
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
// Get categories with their tools
|
||
router.get('/tools', async (req, res) => {
|
||
try {
|
||
const [categories] = await db.pool.execute('SELECT * FROM categories ORDER BY sort_order');
|
||
const [tools] = await db.pool.execute('SELECT * FROM tools WHERE is_enabled = 1 ORDER BY sort_order');
|
||
|
||
const result = categories.map(cat => ({
|
||
...cat,
|
||
tools: tools.filter(t => t.category_id === cat.id),
|
||
}));
|
||
|
||
res.json(result);
|
||
} catch (err) {
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
// Get content blocks by section
|
||
router.get('/content/:section', async (req, res) => {
|
||
try {
|
||
const [rows] = await db.pool.execute(
|
||
'SELECT block_key, title, body, sort_order FROM content_blocks WHERE section = ? AND is_visible = 1 ORDER BY sort_order',
|
||
[req.params.section]
|
||
);
|
||
res.json(rows);
|
||
} catch (err) {
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
|
||
// AI explain regex (proxies to local llama.cpp)
|
||
const rateLimit = require('express-rate-limit');
|
||
const aiLimiter = rateLimit({
|
||
keyGenerator: (req) => (req.session && req.session.user && req.session.user.id) ? 'user_' + req.session.user.id : req.ip,
|
||
windowMs: 60000, max: 10, message: { error: 'Слишком много запросов к AI' },
|
||
});
|
||
|
||
router.post('/ai/explain', express.json(), aiLimiter, async (req, res) => {
|
||
const { pattern, flags } = req.body;
|
||
if (!pattern || typeof pattern !== 'string' || pattern.length > 5000) {
|
||
return res.status(400).json({ error: 'Паттерн пустой или слишком длинный' });
|
||
}
|
||
|
||
let messages;
|
||
|
||
if (flags === 'security-audit') {
|
||
// Security audit mode — pattern contains full HTTP response info
|
||
messages = [
|
||
{ role: 'system', content: 'Ты эксперт по веб-безопасности. Отвечай на русском, markdown. Будь КРАТОК — только реальные проблемы, не перечисляй то что настроено правильно. Максимум 5 пунктов.' },
|
||
{ role: 'user', content: `Проведи security-аудит этого HTTP-ответа:
|
||
|
||
${pattern}
|
||
|
||
Ответь КРАТКО по структуре:
|
||
|
||
## Оценка: [КРИТИЧНО / СРЕДНЕ / ХОРОШО]
|
||
|
||
## Проблемы (только реальные, макс 5):
|
||
- Проблема → как исправить (одной строкой)
|
||
|
||
## Что ОК (одной строкой, перечислением):
|
||
- Какие заголовки безопасности настроены правильно
|
||
|
||
НЕ разбирай каждый заголовок отдельно. НЕ повторяйся. Только практичные проблемы.` },
|
||
];
|
||
} else {
|
||
// Regex explain mode
|
||
messages = [
|
||
{ role: 'system', content: 'Ты эксперт по регулярным выражениям. Отвечай на русском. Формат ответа: markdown без обёртки в блок кода.' },
|
||
{ role: 'user', content: `Regex: /${pattern}/${flags || 'g'}
|
||
|
||
1. Одним предложением: что это выражение ДЕЛАЕТ? Какую задачу решает?
|
||
2. Примеры строк которые СОВПАДУТ с этим выражением (2-3 примера)
|
||
3. Примеры строк которые НЕ совпадут (2-3 примера)
|
||
4. Разбор по частям (кратко, таблицей): часть паттерна → что значит` },
|
||
];
|
||
}
|
||
|
||
// SSE streaming
|
||
res.setHeader('Content-Type', 'text/event-stream');
|
||
res.setHeader('Cache-Control', 'no-cache');
|
||
res.setHeader('Connection', 'keep-alive');
|
||
res.flushHeaders();
|
||
|
||
try {
|
||
const controller = new AbortController();
|
||
const timeout = setTimeout(() => controller.abort(), 120000);
|
||
|
||
// AI endpoints: PC (powerful) → RPi (fallback)
|
||
const AI_ENDPOINTS = [
|
||
{ url: 'http://192.168.31.100:11434/v1/chat/completions', model: 'qwen2.5:14b', name: 'PC' },
|
||
{ url: 'http://127.0.0.1:8080/v1/chat/completions', model: 'qwen2.5-3b', name: 'RPi' },
|
||
];
|
||
|
||
let response = null;
|
||
let usedEndpoint = null;
|
||
|
||
for (const ep of AI_ENDPOINTS) {
|
||
try {
|
||
const pingCtrl = new AbortController();
|
||
const pingTimeout = setTimeout(() => pingCtrl.abort(), 3000);
|
||
const ping = await fetch(ep.url.replace('/v1/chat/completions', '/v1/models'), { signal: pingCtrl.signal });
|
||
clearTimeout(pingTimeout);
|
||
if (!ping.ok) continue;
|
||
|
||
response = await fetch(ep.url, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
signal: controller.signal,
|
||
body: JSON.stringify({
|
||
model: ep.model,
|
||
messages,
|
||
max_tokens: 1000,
|
||
temperature: 0.3,
|
||
stream: true,
|
||
}),
|
||
});
|
||
usedEndpoint = ep;
|
||
break;
|
||
} catch (pingErr) {
|
||
continue; // try next endpoint
|
||
}
|
||
}
|
||
|
||
if (!response) {
|
||
res.write('data: {"error":"Все AI серверы недоступны"}\n\n');
|
||
res.end();
|
||
return;
|
||
}
|
||
|
||
clearTimeout(timeout);
|
||
|
||
if (!response.ok) {
|
||
let errMsg = 'LLM API error: ' + response.status;
|
||
try { const errBody = await response.text(); console.error('LLM error:', response.status, errBody.slice(0, 300)); } catch {}
|
||
res.write('data: {"error":"' + errMsg + '"}\n\n');
|
||
res.end();
|
||
return;
|
||
}
|
||
|
||
// Pipe SSE chunks from llama.cpp to client using async iteration
|
||
try {
|
||
for await (const chunk of response.body) {
|
||
const text = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString();
|
||
res.write(text);
|
||
}
|
||
} catch (streamErr) {
|
||
res.write('data: {"error":"Stream error"}\n\n');
|
||
}
|
||
res.write('data: [DONE]\n\n');
|
||
res.end();
|
||
|
||
req.on('close', () => {
|
||
controller.abort();
|
||
});
|
||
|
||
} catch (err) {
|
||
if (err.name === 'AbortError') {
|
||
res.write('data: {"error":"AI не ответил за 2 минуты"}\n\n');
|
||
} else {
|
||
res.write('data: {"error":"AI недоступен"}\n\n');
|
||
}
|
||
res.end();
|
||
}
|
||
});
|
||
|
||
module.exports = router;
|