wa-dev-tools/routes/api.js

187 lines
6.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;