feat(feedback): personal AI models, AI run tracking and profiles/options endpoint

- user_settings allowlist: feedback_models (up to 10 personal model configs)
- normalizeFeedbackModels() validation, UI block "Мои модели" in #fbPromptModal
- GET /api/ai/profiles/options for tutors (no api_key), used by feedback/shorts
- feedbacks columns ai_prompt/ai_model/ai_duration_ms + backup/restore support
- "ИИ-запрос" popover on feedback card with status, model, timing and prompt
- audit: feedback.update / feedback.ai.run / feedback.ai.error labels
This commit is contained in:
dev
2026-10-07 16:30:27 +03:00
parent a73b1b3f8c
commit a9fb7aaf77
13 changed files with 522 additions and 85 deletions
+8 -2
View File
@@ -189,10 +189,16 @@ This document defines how AI agents should work with the WhatIDo codebase. Follo
### 3h. Персональные настройки и промпт отзыва (`server.js`, `worker.js`, `public/feedback.html`, `public/js/feedback.js`)
- **Таблица `user_settings`** (`user_id`, `key`, `value`, `updated_at`, `PK(user_id, key)`, FK на `users` с `ON DELETE CASCADE`) — персональные настройки пользователя. Схема в трёх местах: `db/init.sql`, `db/migration.sql` и `ensureUserTables()` в `server.js`
- **Эндпоинты `GET`/`PUT /api/user-settings`** под `requireAuth` — это **не** admin-only `PUT /api/settings`, глобальную таблицу `settings` они не трогают. Allowlist ключей: `feedback_prompt` (≤8000 символов) и `feedback_profile` (≤100); неизвестный ключ — 400; пустое значение удаляет строку (семантика «как в системе»). Аудит `user_settings.update`
- **Эндпоинты `GET`/`PUT /api/user-settings`** под `requireAuth` — это **не** admin-only `PUT /api/settings`, глобальную таблицу `settings` они не трогают. Allowlist ключей: `feedback_prompt` (≤8000 символов), `feedback_profile` (≤100) и `feedback_models` (≤16000, структурная валидация); неизвестный ключ — 400; пустое значение удаляет строку (семантика «как в системе»). Аудит `user_settings.update`
- **Личные модели (`feedback_models`)**: JSON-массив `{ id, name, base_url, model, api_key, max_tokens }` в `user_settings`, до 10 штук; валидация — `normalizeFeedbackModels()` в `server.js` (id `^[\w-]+$` ≤40 и без дублей, name 1–64, model 1–100, base_url только http/https ≤300, api_key ≤256, max_tokens 16–131072 или null), ошибка — 400. UI — блок «Мои модели» в `#fbPromptModal` (`#fbModelsList` + форма `#fbModelEdit`), правки сохраняются вместе с «Сохранить» в `savePromptSettings()`. Селекты `#fbModel` и `#fbDefaultModel` собираются с optgroup «Мои модели» **первым**, затем «Системные» (`loadAiProfiles()`) — личные имеют приоритет, системные (`ai_profiles`/native) остаются фолбэком. Резолв в feedback-воркере: `resolveActiveProfile(profileId, authorId)` → личные модели автора (`getPersonalModels` по `feedbacks.author_id`) → системные `ai_profiles` → активная → native
- **Наполнение селектов — только `GET /api/ai/profiles/options`** (`requireAuth`, отдаёт `active`, `native{id,name,model}` и `profiles[{id,name,model}]` **без** `base_url`/`api_key`). `GET /api/ai/profiles` остаётся `requireAdmin` (в нём есть `api_key`) — на него тьютор получает 403, и раньше `loadAiProfiles()` молча оставлял селекты пустыми: `savePromptSettings()` отправлял `feedback_profile: ''`, сервер удалял строку в `user_settings`, и выбор модели «не сохранялся в БД». Тот же эндпоинт используется в `public/js/shorts.js` (`openShortsAi()`)
- **Сохранённое значение не должно теряться**: в `openPromptModal()` → `fillDefaultModelOptions()` значение из `user_settings`, отсутствующее в опциях (удалённый/переименованный профиль), добавляется как опция «… (недоступна)» — без неё `dst.value = X` молча обнулялся, и следующее сохранение стирало `feedback_profile`. После добавления/удаления личной модели в модалке селекты пересобираются (`refreshModelSelects()`), иначе новая модель недоступна как «Модель по умолчанию» до перезагрузки страницы
- **Цепочка промпта отзыва** (`resolveFeedbackPrompt` в `worker.js`): персональный `user_settings.feedback_prompt` по `feedbacks.author_id` → глобальный `feedback_ai_prompt` → `FEEDBACK_AI_DEFAULT_PROMPT`. Шаблон индивидуален для каждого пользователя и применяется ко всем отзывам, которые он авторит
- **Ключи подстановки**: `{group}`, `{date}`, `{resident}`, `{topic}`, `{past_review}`, `{shorts}`, `{context}`, `{text}`. Если шаблон содержит хотя бы один из них — значения подставляются по ключам и блок `КОНТЕКСТ:` в конец **не** добавляется; без ключей — добавляется (поведение до появления ключей). Набор ключей продублирован в трёх местах: `worker.js` (regex + карта `keys`), `public/feedback.html` (`#fbPromptModal`, список с описаниями) и здесь — при изменении набора править все три
- **UI**: шестерёнка `#fbPromptBtn` в шапке `#feedbackModal`, модалка `#fbPromptModal` («Модель по умолчанию» → `feedback_profile`, шаблон → `feedback_prompt`, описание ключей, счётчик 8000, «Сбросить» очищает оба поля). Персональная модель применяется в `resetForm()` к `#fbModel`, пустое значение = активная модель системы
- **UI**: шестерёнка `#fbPromptBtn` в шапке `#feedbackModal`, модалка `#fbPromptModal` («Модель по умолчанию» → `feedback_profile`, шаблон → `feedback_prompt`, описание ключей, счётчик 8000, «Сбросить» очищает оба поля). Персональная модель применяется в `resetForm()` к `#fbModel`, пустое значение = активная модель системы. Селект `#fbModel` отправляется как `ai_profile` и при создании, и при редактировании; `PUT /api/feedbacks/:id` сохраняет его (раньше терял), а при редактировании `loadFeedback()` подставляет модель из записи
- **Учёт запуска ИИ**: колонки `feedbacks.ai_prompt` (итоговый system-промпт после подстановки ключей), `ai_model` (имя модели, реально вызванной через `callModel`), `ai_duration_ms` (время ответа). Пишет их `finish()` воркера; схема — в `db/init.sql`, `db/migration.sql`, `ensureFeedbacksTable()`, SELECT `GET /api/feedbacks` и restore-INSERT
- **Кнопка «ИИ-запрос»** на карточке отзыва (`data-aiinfo` + поповер `.fb-ai-pop` в `public/js/feedback.js`): показывает статус, модель, время ответа, время проверки, ошибку и отправленный промпт. Появляется при `ai_status !== 'none'` или наличии `ai_prompt`; попадает в разметку `fbItemHtml()`, закрывается по клику вне/Escape
- **Аудит запуска**: воркер пишет `feedback.ai.run` (id, status, attempt, model, profile, duration_ms, prompt ≤12000, input ≤6000, output ≤6000, changed, len_before/len_after) или `feedback.ai.error` (id, attempt, error ≤500, prompt, input) — `feedback.ai.generate` из `onFeedbackDone` удалён как дублирующий. `PUT /api/feedbacks/:id` пишет `feedback.update` (id, ai_check, ai_profile); `feedback.create` включает `ai_profile`
- **Бэкап**: `user_settings` входит в `BACKUP_TABLES`, дамп в `buildBackupArchive`, нормализацию в `normalizeRestoreData`, `DELETE` перед `DELETE FROM users` и `INSERT` после `users` в restore (с проверкой существования пользователя)
---