feat(api): управление ИИ-воркерами через внешний API

Внешние системы не могли разбудить воркер, переочередить упавшие
задания или отправить запись на повторную ИИ-проверку: все эти роуты
существовали только во внутреннем API под requireAdmin.

Добавлено на apiV1 (все под apiWrite('write')):
- POST /ai/wake, /photo-jobs/wake — пинок воркеров
- POST /ai/requeue-failed, /photo-jobs/requeue-failed — error -> pending
- POST /entries/:id/ai/recheck — повторная проверка конкретной записи

Филиальная изоляция (главное в этом изменении):
- внутренние requeue-failed делают UPDATE по всей таблице; перенос их
  как есть позволил бы ключу с ограничением по филиалу переочередить
  чужие задания, что ломает правило «ключ не шире выдавшего»
- добавлен хелпер apiBranchClause(user, expr, params): пустая строка
  для admin, AND FALSE при пустом списке филиалов, иначе
  AND <expr> = ANY($N::int[]); применён к обоим массовым UPDATE
- entries фильтруется через groups.branch_id, photo_jobs — через
  photo_jobs -> entries -> groups

Аудит через apiAudit() с префиксом api., метки добавлены в
public/js/audit.js; после мутаций invalidateEntries/invalidateStats
и broadcastEntryChanged.

Воркер отчётов о занятии wake-эндпоинта не получает: он будится сам
из POST/PUT /lesson-reports при ai_check === true.

Документация: таблица эндпоинтов и раздел про воркеров в README.md,
правило apiBranchClause в AGENTS.md 3f.

Проверено: изолированный тест на двух филиалах — requeue-failed
ключом одного филиала вернул count 1 из двух ошибочных заданий,
запись и фото-джоб чужого филиала остались в error, recheck чужой
записи 403; api-keys.selftest.js 61 PASS, api.smoketest.js 76 PASS,
регрессий нет.

Замечание: server.js запечён в образ, compose монтирует только
uploads/, поэтому restart правку не подхватит — нужен
./scripts/deploy.sh или docker compose up -d --build app.
This commit is contained in:
dev
2026-10-05 00:06:38 +03:00
parent cd40260b68
commit 5667198c9b
5 changed files with 138 additions and 0 deletions
+11
View File
@@ -634,9 +634,20 @@ curl -H "Authorization: Bearer wsk_ВАШ_КЛЮЧ" https://ВАШ_ДОМЕН/ap
| `GET` | `/api/v1/lesson-reports`, `/lesson-reports/:id` | чтение |
| `POST`, `PUT`, `DELETE` | `/api/v1/lesson-reports[/:id]` | запись |
| `GET` | `/api/v1/stats` | чтение |
| `POST` | `/api/v1/ai/wake`, `/photo-jobs/wake` | запись |
| `POST` | `/api/v1/ai/requeue-failed`, `/photo-jobs/requeue-failed` | запись |
| `POST` | `/api/v1/entries/:id/ai/recheck` | запись |
Списки возвращают единый формат `{ items, total, limit, offset }`; поддерживаются `limit`/`offset` (до 500) и фильтры (`group_id`, `module_id`, `student_name`, `search`, `date_from`, `date_to`).
Управление ИИ-воркерами:
- `POST /ai/wake` и `POST /photo-jobs/wake` — разбудить воркер проверки текста записей и фото-воркер. Это только пинок: задачи всё равно подхватятся по своему циклу опроса, задержка возможна при недоступном Redis.
- `POST /ai/requeue-failed` и `POST /photo-jobs/requeue-failed` — вернуть в очередь задания со статусом `error`; в ответе `{ ok, count }`.
- `POST /entries/:id/ai/recheck` — отправить конкретную запись на повторную ИИ-проверку.
Все четыре требуют скоуп `write`. Массовые операции уважают филиалы ключа: `requeue-failed` переочередит только записи и фото-задания в доступных филиалах, а не во всей системе. Воркер отчётов о занятии отдельного `wake`-эндпоинта не имеет — он будится сам при `POST`/`PUT /lesson-reports` с `ai_check: true` (значение должно быть именно boolean `true`).
Меры безопасности:
- **Права**: у ключа есть скоупы `read` и `write`; без `write` все изменения возвращают `403`.