Воркер и сервер принимают параметры ИИ-обработки фото: модель апскейла (x2plus / general-x4v3 / animevideo-v3), режим лиц (off / face / all), модель лиц (gfpgan / codeformer) и strength. Пустое тело запроса ведёт себя как раньше: action='ai', params=NULL (инвариант I2). worker.js: - таймаут выбирается по params.face: PHOTO_AI_TIMEOUT_MS для апскейла, PHOTO_AI_FACE_TIMEOUT_MS (600000) для face-режима - runAiEnhance шлёт model/face/face_model/strength и понимает оба контракта: JSON с image_base64 и сырой image/jpeg старого сервиса - тело не-2xx ответа больше не выбрасывается: readErrorBody() добавляет причину к сообщению, иначе оператор видит «ИИ-сервис ответил 400» без объяснения - applyResult пишет в аудит model/face/face_model/device/faces_found/ elapsed_ms/warnings и выбирает текст уведомления по факту режима; warnings видны оператору, если лица не нашлись - CONFIG: + face_timeout_ms, default_model, face_model server.js: - POST /api/entries/:id/photo/enhance-ai принимает и валидирует тело до запроса записи — невалидный вход даёт 400, а не 404/500 - PHOTO_JOB_ACTIONS вынесен на уровень модуля, + ai_face и ai_upscale - GET /api/photo-ai/health (requireAdmin) — прямой прокси /health - photoAiHealth(timeoutMs), в «Статусе стека» вызывается с 2000 мс - getStackInfo(): блок photo_ai (engine, host, device, vram, модели) - настройки photo_ai_face_mode / photo_ai_face_model / photo_ai_device_pref с валидацией в PUT /api/settings, дефолты в init.sql, migration.sql, public-settings и ensurePhotoJobsTable() Приёмка (живой стек, CPU + отдельно CUDA) — в TODO_PHOTO_FACE_AI.md, журнал раздела 4: I1 байт-в-байт 5/5 и совпадение sha256 с raw-путём, I2, I3 при пустом PHOTO_AI_URL, I5 на обрыве и на 503 с Retry-After, 7 невалидных тел → 400, api.smoketest.js 57 PASS.
307 lines
14 KiB
PL/PgSQL
307 lines
14 KiB
PL/PgSQL
CREATE TABLE IF NOT EXISTS students (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(150) NOT NULL UNIQUE,
|
|
created_at TIMESTAMPTZ DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS banned_ips (
|
|
ip VARCHAR(64) PRIMARY KEY,
|
|
reason VARCHAR(100) NOT NULL,
|
|
banned_until TIMESTAMPTZ NOT NULL,
|
|
created_at TIMESTAMPTZ DEFAULT now()
|
|
);
|
|
|
|
ALTER TABLE students ADD COLUMN IF NOT EXISTS group_id INT REFERENCES groups(id);
|
|
ALTER TABLE students ADD COLUMN IF NOT EXISTS photo_path VARCHAR(255);
|
|
ALTER TABLE students ADD COLUMN IF NOT EXISTS profile JSONB;
|
|
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT
|
|
);
|
|
|
|
INSERT INTO settings (key, value) VALUES ('spam_interval_min', '30')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
INSERT INTO settings (key, value) VALUES ('ai_prompt', 'Ты — редактор текстов. Исправь ТОЛЬКО грамматические, орфографические и пунктуационные ошибки в тексте. Приведи к правильному регистру буквы. НЕ меняй слова, структуру предложений, стиль или смысл текста. Верни ТОЛЬКО исправленный текст без пояснений.')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
UPDATE settings
|
|
SET value = 'Ты — редактор текстов. Исправь ТОЛЬКО грамматические, орфографические и пунктуационные ошибки в тексте. Приведи к правильному регистру буквы. НЕ меняй слова, структуру предложений, стиль или смысл текста. Верни ТОЛЬКО исправленный текст без пояснений.'
|
|
WHERE key = 'ai_prompt' AND value IN (
|
|
'Ты — редактор текстов для педагогического журнала. Исправь грамматические, орфографические и пунктуационные ошибки. Сохрани смысл и стиль. Верни ТОЛЬКО исправленный текст, без пояснений.',
|
|
'Ты — редактор текстов. Исправь ошибки. Верни ТОЛЬКО исправленный текст.'
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS share_links (
|
|
id SERIAL PRIMARY KEY,
|
|
token VARCHAR(40) UNIQUE NOT NULL,
|
|
name VARCHAR(200) NOT NULL,
|
|
group_id INT REFERENCES groups(id),
|
|
student_name VARCHAR(150),
|
|
date_from DATE,
|
|
date_to DATE,
|
|
created_at TIMESTAMPTZ DEFAULT now()
|
|
);
|
|
|
|
ALTER TABLE share_links ADD COLUMN IF NOT EXISTS message TEXT;
|
|
ALTER TABLE share_links ADD COLUMN IF NOT EXISTS link_url VARCHAR(500);
|
|
ALTER TABLE share_links ADD COLUMN IF NOT EXISTS show_student_message BOOLEAN;
|
|
ALTER TABLE share_links ADD COLUMN IF NOT EXISTS show_entry_date BOOLEAN;
|
|
ALTER TABLE share_links ADD COLUMN IF NOT EXISTS show_group_photos BOOLEAN;
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'share_links' AND column_name = 'anonymize_names') THEN
|
|
ALTER TABLE share_links ADD COLUMN IF NOT EXISTS show_student_names BOOLEAN;
|
|
UPDATE share_links SET show_student_names = (NOT anonymize_names) WHERE anonymize_names IS NOT NULL;
|
|
ALTER TABLE share_links DROP COLUMN anonymize_names;
|
|
END IF;
|
|
END $$;
|
|
|
|
DO $$
|
|
BEGIN
|
|
INSERT INTO students (name)
|
|
SELECT DISTINCT student_name FROM entries WHERE student_name IS NOT NULL AND student_name <> ''
|
|
ON CONFLICT (name) DO NOTHING;
|
|
END $$;
|
|
|
|
CREATE TABLE IF NOT EXISTS project_files (
|
|
id SERIAL PRIMARY KEY,
|
|
entry_id INT NOT NULL REFERENCES entries(id) ON DELETE CASCADE,
|
|
token VARCHAR(64),
|
|
path VARCHAR(255) NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
created_at TIMESTAMPTZ DEFAULT now()
|
|
);
|
|
|
|
UPDATE project_files SET token = md5(random()::text || clock_timestamp()::text) WHERE token IS NULL;
|
|
ALTER TABLE project_files ALTER COLUMN token SET NOT NULL;
|
|
CREATE UNIQUE INDEX IF NOT EXISTS project_files_token_key ON project_files(token);
|
|
|
|
ALTER TABLE project_files ALTER COLUMN entry_id DROP NOT NULL;
|
|
ALTER TABLE project_files ADD COLUMN IF NOT EXISTS detached_at TIMESTAMPTZ;
|
|
|
|
CREATE TABLE IF NOT EXISTS entry_photos (
|
|
id SERIAL PRIMARY KEY,
|
|
entry_id INT NOT NULL REFERENCES entries(id) ON DELETE CASCADE,
|
|
photo_path VARCHAR(255) NOT NULL,
|
|
caption TEXT,
|
|
sort_order INT DEFAULT 0,
|
|
created_at TIMESTAMPTZ DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_entry_photos_entry_id ON entry_photos(entry_id);
|
|
|
|
ALTER TABLE entries ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
|
ALTER TABLE entries ADD COLUMN IF NOT EXISTS purge_at TIMESTAMPTZ;
|
|
ALTER TABLE entries ADD COLUMN IF NOT EXISTS description_original TEXT;
|
|
ALTER TABLE entries ADD COLUMN IF NOT EXISTS description_ai TEXT;
|
|
ALTER TABLE entries ADD COLUMN IF NOT EXISTS ai_status VARCHAR(20) NOT NULL DEFAULT 'pending';
|
|
ALTER TABLE entries ADD COLUMN IF NOT EXISTS ai_checked_at TIMESTAMPTZ;
|
|
ALTER TABLE entries ADD COLUMN IF NOT EXISTS ai_error TEXT;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_entries_ai_pending ON entries(id) WHERE ai_status = 'pending' AND deleted_at IS NULL;
|
|
|
|
INSERT INTO settings (key, value) VALUES ('ai_autocheck_enabled', 'true')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
|
|
INSERT INTO settings (key, value) VALUES ('share_show_student_message', 'false')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
INSERT INTO settings (key, value) VALUES ('share_show_entry_date', 'false')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
INSERT INTO settings (key, value) VALUES ('share_show_student_names', 'true')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
DELETE FROM settings WHERE key = 'share_anonymize_names';
|
|
INSERT INTO settings (key, value) VALUES ('share_show_group_photos', 'true')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
INSERT INTO settings (key, value) VALUES ('cookie_notice_text', 'Этот сайт использует cookie-файлы для корректной работы. Продолжая просмотр, вы соглашаетесь с их использованием.')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
|
|
INSERT INTO settings (key, value) VALUES ('camera_enabled', 'true')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
|
|
INSERT INTO settings (key, value) VALUES ('trash_purge_days', '30')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM settings WHERE key = 'ai_autocheck_migrated') THEN
|
|
UPDATE entries SET description_original = description WHERE description_original IS NULL;
|
|
UPDATE entries SET ai_status = 'skipped' WHERE ai_status = 'pending';
|
|
INSERT INTO settings (key, value) VALUES ('ai_autocheck_migrated', '1');
|
|
END IF;
|
|
END $$;
|
|
|
|
CREATE TABLE IF NOT EXISTS branches (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(200) NOT NULL UNIQUE,
|
|
address TEXT,
|
|
phone VARCHAR(50),
|
|
created_at TIMESTAMPTZ DEFAULT now()
|
|
);
|
|
|
|
ALTER TABLE groups ADD COLUMN IF NOT EXISTS branch_id INT REFERENCES branches(id) ON DELETE SET NULL;
|
|
ALTER TABLE groups ADD COLUMN IF NOT EXISTS day_of_week INT;
|
|
ALTER TABLE groups ADD COLUMN IF NOT EXISTS time_start TIME;
|
|
ALTER TABLE groups ADD COLUMN IF NOT EXISTS time_end TIME;
|
|
ALTER TABLE groups ADD COLUMN IF NOT EXISTS cover_path VARCHAR(255);
|
|
ALTER TABLE groups ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
|
ALTER TABLE groups ADD COLUMN IF NOT EXISTS purge_at TIMESTAMPTZ;
|
|
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id SERIAL PRIMARY KEY,
|
|
username VARCHAR(100) NOT NULL UNIQUE,
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
name VARCHAR(150),
|
|
role VARCHAR(20) NOT NULL DEFAULT 'tutor' CHECK (role IN ('admin','tutor')),
|
|
is_active BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMPTZ DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_branches (
|
|
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
branch_id INT NOT NULL REFERENCES branches(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (user_id, branch_id)
|
|
);
|
|
|
|
ALTER TABLE groups ADD COLUMN IF NOT EXISTS tutor_id INT REFERENCES users(id) ON DELETE SET NULL;
|
|
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
token VARCHAR(64) NOT NULL UNIQUE,
|
|
created_at TIMESTAMPTZ DEFAULT now(),
|
|
expires_at TIMESTAMPTZ NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token);
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
|
|
|
|
ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS user_id INT REFERENCES users(id) ON DELETE SET NULL;
|
|
|
|
CREATE TABLE IF NOT EXISTS group_photos (
|
|
id SERIAL PRIMARY KEY,
|
|
group_id INT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
|
photo_path VARCHAR(255) NOT NULL,
|
|
caption TEXT,
|
|
taken_at DATE,
|
|
created_at TIMESTAMPTZ DEFAULT now()
|
|
);
|
|
|
|
ALTER TABLE group_photos ADD COLUMN IF NOT EXISTS sort_order INT DEFAULT 0;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_group_photos_group_id ON group_photos(group_id);
|
|
|
|
UPDATE entries e SET photo_path = p.photo_path
|
|
FROM (
|
|
SELECT DISTINCT ON (entry_id) entry_id, photo_path
|
|
FROM entry_photos ORDER BY entry_id, sort_order, id
|
|
) p
|
|
WHERE e.photo_path IS NULL AND p.entry_id = e.id;
|
|
|
|
CREATE OR REPLACE FUNCTION notify_entries_changed() RETURNS trigger AS $$
|
|
BEGIN
|
|
IF (TG_OP = 'INSERT') THEN
|
|
PERFORM pg_notify('entries_changed', json_build_object('type', 'entry_created', 'id', NEW.id)::text);
|
|
ELSIF (TG_OP = 'UPDATE' AND OLD.ai_status IS DISTINCT FROM NEW.ai_status) THEN
|
|
PERFORM pg_notify('entries_changed', json_build_object('type', 'ai_status', 'id', NEW.id, 'status', NEW.ai_status, 'error', NEW.ai_error, 'description', NEW.description, 'description_ai', NEW.description_ai, 'description_original', NEW.description_original)::text);
|
|
END IF;
|
|
RETURN NULL;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS trg_entries_notify ON entries;
|
|
CREATE TRIGGER trg_entries_notify AFTER INSERT OR UPDATE OF ai_status ON entries
|
|
FOR EACH ROW EXECUTE FUNCTION notify_entries_changed();
|
|
|
|
CREATE TABLE IF NOT EXISTS photo_jobs (
|
|
id SERIAL PRIMARY KEY,
|
|
entry_id INT NOT NULL REFERENCES entries(id) ON DELETE CASCADE,
|
|
action VARCHAR(20) NOT NULL DEFAULT 'ai',
|
|
params JSONB,
|
|
before_path VARCHAR(255),
|
|
after_path VARCHAR(255),
|
|
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
|
applied BOOLEAN NOT NULL DEFAULT false,
|
|
attempts INT NOT NULL DEFAULT 0,
|
|
error TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT now(),
|
|
finished_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_photo_jobs_pending ON photo_jobs(id) WHERE status = 'pending';
|
|
CREATE INDEX IF NOT EXISTS idx_photo_jobs_entry_id ON photo_jobs(entry_id);
|
|
|
|
INSERT INTO settings (key, value) VALUES ('photo_worker_enabled', 'true')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
|
|
INSERT INTO settings (key, value) VALUES ('photo_ai_face_mode', 'off')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
|
|
INSERT INTO settings (key, value) VALUES ('photo_ai_face_model', 'gfpgan')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
|
|
INSERT INTO settings (key, value) VALUES ('photo_ai_device_pref', 'auto')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
|
|
ALTER TABLE entries ADD COLUMN IF NOT EXISTS photo_original_path VARCHAR(255);
|
|
ALTER TABLE photo_jobs ADD COLUMN IF NOT EXISTS applied BOOLEAN NOT NULL DEFAULT false;
|
|
|
|
CREATE TABLE IF NOT EXISTS modules (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(200) NOT NULL UNIQUE,
|
|
lessons_count INT NOT NULL DEFAULT 0,
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
created_at TIMESTAMPTZ DEFAULT now()
|
|
);
|
|
|
|
ALTER TABLE modules ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT true;
|
|
ALTER TABLE modules ADD COLUMN IF NOT EXISTS photo_path VARCHAR(255);
|
|
ALTER TABLE entries ADD COLUMN IF NOT EXISTS module_id INT REFERENCES modules(id) ON DELETE SET NULL;
|
|
CREATE INDEX IF NOT EXISTS idx_entries_module_id ON entries(module_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS student_photos (
|
|
id SERIAL PRIMARY KEY,
|
|
student_id INT NOT NULL REFERENCES students(id) ON DELETE CASCADE,
|
|
photo_path VARCHAR(255) NOT NULL,
|
|
created_at TIMESTAMPTZ DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_student_photos_student_id ON student_photos(student_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS notifications (
|
|
id SERIAL PRIMARY KEY,
|
|
type VARCHAR(50) NOT NULL,
|
|
level VARCHAR(20) NOT NULL DEFAULT 'info',
|
|
title VARCHAR(200) NOT NULL,
|
|
body TEXT,
|
|
link VARCHAR(255),
|
|
target JSONB,
|
|
admin_only BOOLEAN NOT NULL DEFAULT false,
|
|
branch_id INT REFERENCES branches(id) ON DELETE SET NULL,
|
|
created_at TIMESTAMPTZ DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_notifications_created_at ON notifications(created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_notifications_branch_id ON notifications(branch_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS notification_reads (
|
|
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
notification_id INT NOT NULL REFERENCES notifications(id) ON DELETE CASCADE,
|
|
read_at TIMESTAMPTZ DEFAULT now(),
|
|
PRIMARY KEY (user_id, notification_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_notification_reads_user ON notification_reads(user_id);
|
|
|
|
INSERT INTO settings (key, value) VALUES ('notify_enabled', 'true') ON CONFLICT (key) DO NOTHING;
|
|
INSERT INTO settings (key, value) VALUES ('notify_retention_days', '30') ON CONFLICT (key) DO NOTHING;
|
|
INSERT INTO settings (key, value) VALUES ('notify_entry_new', 'true') ON CONFLICT (key) DO NOTHING;
|
|
INSERT INTO settings (key, value) VALUES ('notify_entry_ai_corrected', 'false') ON CONFLICT (key) DO NOTHING;
|
|
INSERT INTO settings (key, value) VALUES ('notify_entry_ai_error', 'true') ON CONFLICT (key) DO NOTHING;
|
|
INSERT INTO settings (key, value) VALUES ('notify_photo_job_done', 'true') ON CONFLICT (key) DO NOTHING;
|
|
INSERT INTO settings (key, value) VALUES ('notify_photo_job_error', 'true') ON CONFLICT (key) DO NOTHING;
|
|
INSERT INTO settings (key, value) VALUES ('notify_ip_ban', 'true') ON CONFLICT (key) DO NOTHING;
|
|
INSERT INTO settings (key, value) VALUES ('notify_backup_restore', 'true') ON CONFLICT (key) DO NOTHING;
|
|
INSERT INTO settings (key, value) VALUES ('notify_backup_create', 'false') ON CONFLICT (key) DO NOTHING;
|
|
INSERT INTO settings (key, value) VALUES ('notify_system_test', 'true') ON CONFLICT (key) DO NOTHING;
|