Add HF model auto-download with configurable AI env, and share link visibility toggles

This commit is contained in:
dev
2026-09-11 13:12:13 +03:00
parent b7e798b867
commit 275ed46dfb
17 changed files with 365 additions and 67 deletions
+24 -17
View File
@@ -656,7 +656,7 @@ app.get('/api/settings', requireAdmin, async (_, res) => {
});
app.get('/api/public-settings', apiLimiter, async (_, res) => {
const keys = ['footer_left', 'footer_right'];
const keys = ['footer_left', 'footer_right', 'share_show_student_message', 'share_show_entry_date', 'share_show_student_names', 'share_show_group_photos'];
const out = {};
for (const k of keys) out[k] = await getSetting(k, '');
res.json(out);
@@ -715,7 +715,8 @@ const zlib = require('zlib');
const tar = require('tar');
const os = require('os');
const AI_URL = process.env.AI_URL || 'http://text-corrector:8080';
const AI_DEFAULT_PROMPT = 'Ты — редактор текстов для педагогического журнала. Исправь грамматические, орфографические и пунктуационные ошибки. Сохрани смысл и стиль. Верни ТОЛЬКО исправленный текст, без пояснений.';
const AI_MODEL = process.env.AI_MODEL || 'qwen2.5-1.5b-instruct-q4_k_m.gguf';
const AI_DEFAULT_PROMPT = process.env.AI_PROMPT || 'Ты — редактор текстов. Исправь ТОЛЬКО грамматические, орфографические и пунктуационные ошибки в тексте. Приведи к правильному регистру буквы. НЕ меняй слова, структуру предложений, стиль или смысл текста. Верни ТОЛЬКО исправленный текст без пояснений.';
let entryAutoChecker = null;
async function getAiPrompt() {
@@ -730,7 +731,7 @@ async function aiCorrectText(text) {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'qwen2.5-1.5b-instruct',
model: AI_MODEL,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: text }
@@ -1148,7 +1149,7 @@ app.get('/api/links', requireAuth, async (req, res) => {
});
app.post('/api/links', requireAuth, async (req, res) => {
const { name, group_id, student_name, date_from, date_to, anonymize_names, expires_at, access_password } = req.body;
const { name, group_id, student_name, date_from, date_to, expires_at, access_password } = req.body;
if (!name?.trim()) return res.status(400).json({ error: 'Название обязательно' });
let message, linkUrl;
try {
@@ -1178,16 +1179,16 @@ app.post('/api/links', requireAuth, async (req, res) => {
expiresAt = defaultExp.toISOString();
}
const { rows } = await pool.query(
`INSERT INTO share_links (token, name, group_id, student_name, date_from, date_to, anonymize_names, expires_at, access_password_hash, message, link_url)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *`,
[token, name.trim(), group_id || null, student_name || null, date_from || null, date_to || null, anonymize_names || false, expiresAt, passwordHash, message, linkUrl]
`INSERT INTO share_links (token, name, group_id, student_name, date_from, date_to, show_student_names, expires_at, access_password_hash, message, link_url, show_student_message, show_entry_date, show_group_photos)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING *`,
[token, name.trim(), group_id || null, student_name || null, date_from || null, date_to || null, req.body.show_student_names == null ? null : !!req.body.show_student_names, expiresAt, passwordHash, message, linkUrl, req.body.show_student_message == null ? null : !!req.body.show_student_message, req.body.show_entry_date == null ? null : !!req.body.show_entry_date, req.body.show_group_photos == null ? null : !!req.body.show_group_photos]
);
await logAudit(req, 'link.create', { id: rows[0].id, name: name.trim() });
res.status(201).json(normDates(rows[0]));
});
app.put('/api/links/:id', requireAuth, async (req, res) => {
const { name, group_id, student_name, date_from, date_to, anonymize_names, expires_at, access_password } = req.body;
const { name, group_id, student_name, date_from, date_to, expires_at, access_password } = req.body;
if (!name?.trim()) return res.status(400).json({ error: 'Название обязательно' });
let message, linkUrl;
try {
@@ -1227,9 +1228,9 @@ app.put('/api/links/:id', requireAuth, async (req, res) => {
expiresAt = null; // Clear expiry if null sent
}
}
const fields = ['name = $1', 'group_id = $2', 'student_name = $3', 'date_from = $4', 'date_to = $5', 'anonymize_names = $6', 'message = $7', 'link_url = $8'];
const values = [name.trim(), group_id || null, student_name || null, date_from || null, date_to || null, anonymize_names || false, message, linkUrl];
let paramIdx = 9;
const fields = ['name = $1', 'group_id = $2', 'student_name = $3', 'date_from = $4', 'date_to = $5', 'show_student_names = $6', 'message = $7', 'link_url = $8', 'show_student_message = $9', 'show_entry_date = $10', 'show_group_photos = $11'];
const values = [name.trim(), group_id || null, student_name || null, date_from || null, date_to || null, req.body.show_student_names == null ? null : !!req.body.show_student_names, message, linkUrl, req.body.show_student_message == null ? null : !!req.body.show_student_message, req.body.show_entry_date == null ? null : !!req.body.show_entry_date, req.body.show_group_photos == null ? null : !!req.body.show_group_photos];
let paramIdx = 12;
if (passwordHash !== undefined) {
fields.push(`access_password_hash = $${paramIdx++}`);
values.push(passwordHash);
@@ -1311,8 +1312,10 @@ app.get('/api/share/:token', fileLimiter, async (req, res) => {
}
entries.forEach(r => { r.files = files[r.id] || []; });
// Anonymize names if enabled
if (l.anonymize_names) {
const showStudentNames = l.show_student_names != null ? l.show_student_names : (await getSetting('share_show_student_names', 'true')) !== 'false';
// Anonymize names if disabled
if (!showStudentNames) {
const nameMap = new Map();
let counter = 1;
entries.forEach(e => {
@@ -1342,9 +1345,13 @@ app.get('/api/share/:token', fileLimiter, async (req, res) => {
message: l.message,
link_url: l.link_url,
created_at: l.created_at,
anonymize_names: l.anonymize_names,
expires_at: l.expires_at,
show_student_names: showStudentNames,
show_student_message: l.show_student_message != null ? l.show_student_message : (await getSetting('share_show_student_message', 'false')) === 'true',
show_entry_date: l.show_entry_date != null ? l.show_entry_date : (await getSetting('share_show_entry_date', 'false')) === 'true',
show_group_photos: l.show_group_photos != null ? l.show_group_photos : (await getSetting('share_show_group_photos', 'true')) !== 'false',
entries,
photos,
photos: (l.show_group_photos != null ? l.show_group_photos : (await getSetting('share_show_group_photos', 'true')) !== 'false') ? photos : [],
});
});
@@ -2589,7 +2596,7 @@ app.get('/api/ai/queue', requireAdmin, async (_, res) => {
enabled,
counts,
worker: entryAutoChecker ? entryAutoChecker.getStats() : null,
model: process.env.AI_MODEL || 'qwen2.5-1.5b-instruct',
model: AI_MODEL,
});
});
@@ -2642,7 +2649,7 @@ app.get('/api/ai/status', requireAdmin, async (_, res) => {
const service = await aiHealthCheck();
res.json({
enabled,
model: process.env.AI_MODEL || 'qwen2.5-1.5b-instruct',
model: AI_MODEL,
ai_url: AI_URL,
service,
worker: entryAutoChecker ? entryAutoChecker.getInfo() : null,