Fix: backup/restore now includes group_photos and entry_photos tables; increase AI request timeout to 120s (configurable via AI_REQUEST_TIMEOUT_MS)
This commit is contained in:
+3
-1
@@ -11,4 +11,6 @@ AI_MODEL=qwen2.5-1.5b-instruct-q4_k_m.gguf
|
|||||||
# Репозиторий Hugging Face, откуда скачивается модель
|
# Репозиторий Hugging Face, откуда скачивается модель
|
||||||
AI_MODEL_REPO=Qwen/Qwen2.5-1.5B-Instruct-GGUF
|
AI_MODEL_REPO=Qwen/Qwen2.5-1.5B-Instruct-GGUF
|
||||||
# Промпт по умолчанию для автоисправления текстов (переопределяет встроенный, если в настройках не сохранён свой)
|
# Промпт по умолчанию для автоисправления текстов (переопределяет встроенный, если в настройках не сохранён свой)
|
||||||
AI_PROMPT=Ты — редактор текстов. Исправь ТОЛЬКО грамматические, орфографические и пунктуационные ошибки в тексте. Приведи к правильному регистру буквы. НЕ меняй слова, структуру предложений, стиль или смысл текста. Верни ТОЛЬКО исправленный текст без пояснений.
|
AI_PROMPT=Ты — редактор текстов. Исправь ТОЛЬКО грамматические, орфографические и пунктуационные ошибки в тексте. Приведи к правильному регистру буквы. НЕ меняй слова, структуру предложений, стиль или смысл текста. Верни ТОЛЬКО исправленный текст без пояснений.
|
||||||
|
# Таймаут запроса к AI (мс), по умолчанию 120000 (2 минуты)
|
||||||
|
AI_REQUEST_TIMEOUT_MS=120000
|
||||||
@@ -32,6 +32,7 @@ services:
|
|||||||
BACKUP_UPLOAD_LIMIT_MB: ${BACKUP_UPLOAD_LIMIT_MB:-500}
|
BACKUP_UPLOAD_LIMIT_MB: ${BACKUP_UPLOAD_LIMIT_MB:-500}
|
||||||
AI_MODEL: ${AI_MODEL:-qwen2.5-1.5b-instruct-q4_k_m.gguf}
|
AI_MODEL: ${AI_MODEL:-qwen2.5-1.5b-instruct-q4_k_m.gguf}
|
||||||
AI_PROMPT: ${AI_PROMPT:-}
|
AI_PROMPT: ${AI_PROMPT:-}
|
||||||
|
AI_REQUEST_TIMEOUT_MS: ${AI_REQUEST_TIMEOUT_MS:-120000}
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
TZ: Europe/Moscow
|
TZ: Europe/Moscow
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -810,6 +810,17 @@ function optTime(v) {
|
|||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function optDate(v) {
|
||||||
|
if (v === null || v === undefined) return null;
|
||||||
|
if (typeof v !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(v)) throw new Error('Invalid date');
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
function optBool(v) {
|
||||||
|
if (v === null || v === undefined) return null;
|
||||||
|
return !!v;
|
||||||
|
}
|
||||||
|
|
||||||
function reqToken(v) {
|
function reqToken(v) {
|
||||||
if (typeof v !== 'string' || !/^[0-9a-f]{16,64}$/.test(v)) throw new Error('Invalid token');
|
if (typeof v !== 'string' || !/^[0-9a-f]{16,64}$/.test(v)) throw new Error('Invalid token');
|
||||||
return v;
|
return v;
|
||||||
@@ -898,17 +909,52 @@ function normalizeRestoreData(data) {
|
|||||||
user_id: reqInt(x.user_id),
|
user_id: reqInt(x.user_id),
|
||||||
branch_id: reqInt(x.branch_id),
|
branch_id: reqInt(x.branch_id),
|
||||||
}));
|
}));
|
||||||
|
const entry_photos = (data.entry_photos || []).map(x => ({
|
||||||
|
id: reqInt(x.id),
|
||||||
|
entry_id: reqInt(x.entry_id),
|
||||||
|
photo_path: reqUploadPath(x.photo_path, 255),
|
||||||
|
caption: optStr(x.caption, 10000),
|
||||||
|
sort_order: optInt(x.sort_order, -2147483648, 2147483647),
|
||||||
|
created_at: optTs(x.created_at),
|
||||||
|
}));
|
||||||
|
const group_photos = (data.group_photos || []).map(x => ({
|
||||||
|
id: reqInt(x.id),
|
||||||
|
group_id: reqInt(x.group_id),
|
||||||
|
photo_path: reqUploadPath(x.photo_path, 255),
|
||||||
|
caption: optStr(x.caption, 10000),
|
||||||
|
taken_at: optDate(x.taken_at),
|
||||||
|
sort_order: optInt(x.sort_order, -2147483648, 2147483647),
|
||||||
|
created_at: optTs(x.created_at),
|
||||||
|
}));
|
||||||
|
const share_links = (data.share_links || []).map(x => ({
|
||||||
|
id: reqInt(x.id),
|
||||||
|
token: optStr(x.token, 40),
|
||||||
|
name: reqStr(x.name, 200),
|
||||||
|
group_id: optInt(x.group_id, 0, 2147483647),
|
||||||
|
student_name: optStr(x.student_name, 150),
|
||||||
|
date_from: optDate(x.date_from),
|
||||||
|
date_to: optDate(x.date_to),
|
||||||
|
show_student_names: optBool(x.show_student_names),
|
||||||
|
expires_at: optTs(x.expires_at),
|
||||||
|
access_password_hash: optStr(x.access_password_hash, 255),
|
||||||
|
message: optStr(x.message, 2000),
|
||||||
|
link_url: optStr(x.link_url, 500),
|
||||||
|
show_student_message: optBool(x.show_student_message),
|
||||||
|
show_entry_date: optBool(x.show_entry_date),
|
||||||
|
show_group_photos: optBool(x.show_group_photos),
|
||||||
|
created_at: optTs(x.created_at),
|
||||||
|
}));
|
||||||
const settings = {};
|
const settings = {};
|
||||||
for (const [k, v] of Object.entries(data.settings || {})) {
|
for (const [k, v] of Object.entries(data.settings || {})) {
|
||||||
settings[reqStr(k, 100)] = reqStr(String(v), 10000);
|
settings[reqStr(k, 100)] = reqStr(String(v), 10000);
|
||||||
}
|
}
|
||||||
return { groups, students, entries, project_files, settings, branches, users, user_branches };
|
return { groups, students, entries, project_files, settings, branches, users, user_branches, entry_photos, group_photos, share_links };
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get('/api/backup', requireAdmin, async (req, res) => {
|
app.get('/api/backup', requireAdmin, async (req, res) => {
|
||||||
const staging = fs.mkdtempSync(path.join(os.tmpdir(), 'wido-bk-'));
|
const staging = fs.mkdtempSync(path.join(os.tmpdir(), 'wido-bk-'));
|
||||||
try {
|
try {
|
||||||
const [g, s, e, st, pf, br, us, ub] = await Promise.all([
|
const [g, s, e, st, pf, br, us, ub, gp, ep] = await Promise.all([
|
||||||
pool.query('SELECT * FROM groups ORDER BY id'),
|
pool.query('SELECT * FROM groups ORDER BY id'),
|
||||||
pool.query('SELECT * FROM students ORDER BY id'),
|
pool.query('SELECT * FROM students ORDER BY id'),
|
||||||
pool.query('SELECT * FROM entries ORDER BY id'),
|
pool.query('SELECT * FROM entries ORDER BY id'),
|
||||||
@@ -917,10 +963,12 @@ app.get('/api/backup', requireAdmin, async (req, res) => {
|
|||||||
pool.query('SELECT * FROM branches ORDER BY id'),
|
pool.query('SELECT * FROM branches ORDER BY id'),
|
||||||
pool.query('SELECT * FROM users ORDER BY id'),
|
pool.query('SELECT * FROM users ORDER BY id'),
|
||||||
pool.query('SELECT * FROM user_branches ORDER BY user_id, branch_id'),
|
pool.query('SELECT * FROM user_branches ORDER BY user_id, branch_id'),
|
||||||
|
pool.query('SELECT * FROM group_photos ORDER BY id'),
|
||||||
|
pool.query('SELECT * FROM entry_photos ORDER BY id'),
|
||||||
]);
|
]);
|
||||||
const settings = {};
|
const settings = {};
|
||||||
st.rows.forEach(r => { settings[r.key] = r.value; });
|
st.rows.forEach(r => { settings[r.key] = r.value; });
|
||||||
const payload = { version: 1, created_at: new Date().toISOString(), groups: g.rows, students: s.rows, entries: e.rows, settings, project_files: pf.rows, branches: br.rows, users: us.rows, user_branches: ub.rows };
|
const payload = { version: 1, created_at: new Date().toISOString(), groups: g.rows, students: s.rows, entries: e.rows, settings, project_files: pf.rows, branches: br.rows, users: us.rows, user_branches: ub.rows, group_photos: gp.rows, entry_photos: ep.rows };
|
||||||
fs.writeFileSync(path.join(staging, 'data.json'), JSON.stringify(payload));
|
fs.writeFileSync(path.join(staging, 'data.json'), JSON.stringify(payload));
|
||||||
fs.mkdirSync(path.join(staging, 'uploads'), { recursive: true });
|
fs.mkdirSync(path.join(staging, 'uploads'), { recursive: true });
|
||||||
const dir = path.join(__dirname, 'uploads');
|
const dir = path.join(__dirname, 'uploads');
|
||||||
@@ -1050,6 +1098,18 @@ app.post('/api/restore', requireAdmin, uploadBackup.single('backup'), async (req
|
|||||||
[x.id, x.entry_id, x.token, x.path, x.name, x.created_at]
|
[x.id, x.entry_id, x.token, x.path, x.name, x.created_at]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
for (const x of ndata.group_photos) {
|
||||||
|
await client.query(
|
||||||
|
'INSERT INTO group_photos (id, group_id, photo_path, caption, taken_at, sort_order, created_at) VALUES ($1,$2,$3,$4,$5,$6,$7)',
|
||||||
|
[x.id, x.group_id, x.photo_path, x.caption, x.taken_at, x.sort_order, x.created_at]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const x of ndata.entry_photos) {
|
||||||
|
await client.query(
|
||||||
|
'INSERT INTO entry_photos (id, entry_id, photo_path, caption, sort_order, created_at) VALUES ($1,$2,$3,$4,$5,$6)',
|
||||||
|
[x.id, x.entry_id, x.photo_path, x.caption, x.sort_order, x.created_at]
|
||||||
|
);
|
||||||
|
}
|
||||||
for (const x of ndata.users) {
|
for (const x of ndata.users) {
|
||||||
await client.query(
|
await client.query(
|
||||||
'INSERT INTO users (id, username, password_hash, name, role, is_active, created_at) VALUES ($1,$2,$3,$4,$5,$6,$7)',
|
'INSERT INTO users (id, username, password_hash, name, role, is_active, created_at) VALUES ($1,$2,$3,$4,$5,$6,$7)',
|
||||||
@@ -1068,7 +1128,7 @@ app.post('/api/restore', requireAdmin, uploadBackup.single('backup'), async (req
|
|||||||
[k, String(v ?? '')]
|
[k, String(v ?? '')]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (const tbl of ['groups', 'students', 'entries', 'project_files', 'branches', 'users']) {
|
for (const tbl of ['groups', 'students', 'entries', 'project_files', 'branches', 'users', 'group_photos', 'entry_photos']) {
|
||||||
const r = await client.query('SELECT COALESCE(MAX(id), 1) AS m FROM ' + tbl);
|
const r = await client.query('SELECT COALESCE(MAX(id), 1) AS m FROM ' + tbl);
|
||||||
await client.query('SELECT setval(pg_get_serial_sequence($1, $2), $3)', [tbl, 'id', r.rows[0].m]);
|
await client.query('SELECT setval(pg_get_serial_sequence($1, $2), $3)', [tbl, 'id', r.rows[0].m]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const IDLE_MIN_MS = 2000;
|
const IDLE_MIN_MS = 2000;
|
||||||
const IDLE_MAX_MS = 60000;
|
const IDLE_MAX_MS = 60000;
|
||||||
const REQUEST_TIMEOUT_MS = 30000;
|
const REQUEST_TIMEOUT_MS = parseInt(process.env.AI_REQUEST_TIMEOUT_MS || '120000', 10);
|
||||||
const MAX_INPUT_CHARS = 2000;
|
const MAX_INPUT_CHARS = 2000;
|
||||||
const MIN_TEXT_CHARS = 4;
|
const MIN_TEXT_CHARS = 4;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user