Skip to content

ТЗ — Генерация контента ИИ (cms/ai-content)

Слой: 🟡 функц-модуль · Зрелость доноров: ★★ · Донор: universal/artel (ClaudeArticleGenerator) Статус: спроектировано, внедрение отложено (приоритет P2)

🔄 Ревизия (корпоративный MVP, 15.07.2026): основной целевой сценарий — генерация в свойства записи типа контента по её схеме (cms_content_types.schema, content-types-engine §4); текстовые сущности модулей-потребителей (статьи, описания) и SEO-поля остаются вторым штатным сценарием, оба варианта покрываются одним и тем же AiContentService. На MVP провайдер — только Claude (Anthropic) (ai-content.provider=anthropic): ai-content.fallback_provider и мульти-провайдерность (настройки ниже) объявлены в ТЗ, но вне объёма MVP — включение отдельной задачей после MVP. Статус: спроектировано сейчас, внедрение отложено (P2).

Назначение и возможности

Низкоуровневый сервис генерации текстового контента при помощи ИИ (LLM) для редакторов админки: SEO-мета (title/description/H1), тексты SEO-блоков, описания товаров, черновики статей блога, анонсы (excerpt). Модуль не публикует контент сам — результат генерации всегда черновик, который человек читает, правит и принимает решение сохранить; ни одно поле целевой сущности не перезаписывается автоматически.

Модуль спроектирован как сервис снизу графа зависимостей (см. «Зависимости и выключение»): он ничего не знает про блог, каталог или SEO-аудит — это они, будучи содержательно «выше», по желанию используют его через suggests и сервис-вызов.

  • Каталог видов генерации (kind): seo_meta, seo_text, product_description, article, excerpt — расширяемый enum, новый вид добавляется ревизией модуля, не потребителем.
  • Декларативные шаблоны промптов по kind (cms_ai_content_prompts) с переменными подстановки — сам текст промпта никогда не хардкожен в PHP-коде (anti-hardcode §2 правил проекта): редактируется в админке, версионируется, снимок сохраняется в задачу на момент постановки.
  • Модалка предпросмотра промпта (что именно будет отправлено провайдеру, с уже подставленными переменными) перед постановкой задачи и отдельная модалка предпросмотра результата перед принятием.
  • Асинхронная генерация через очередь ai-content (внешний HTTP — только из джобы, §9 стандарта); результат — запись cms_ai_content_jobs со статусом, черновик правится в UI потребителя, никогда не пишется напрямую в его таблицы.
  • Авто-расчёт excerpt/reading_time при генерации статьи — по образцу донора (ClaudeArticleGenerator): модель возвращает структурированный JSON, excerpt и reading_time вычисляются из него, а не отдельным вызовом.
  • Учёт расхода (токены, стоимость) по задаче и агрегированно за день/за kind; дневной лимит запросов и дневной бюджет — явные квоты (§6 стандарта).
  • Kill-switch — аварийная остановка любой генерации без выключения модуля.
  • Fallback-провайдер (вне MVP, см. ревизию 15.07.2026 в шапке): при недоступности основного провайдера (сбой, лимит, нестабильность зарубежных API из РФ — см. opensource/ai-llm.md) допускается переключение на резервный, без падения задачи; на MVP провайдер только Claude (Anthropic), без fallback.

Зависимости и выключение

requires: ядро (cms/core-contracts) · cms/integrations-busвсе внешние вызовы к провайдеру ИИ идут только через шину (креды, rate-limit, ретраи, circuit breaker, журнал, идемпотентность — модуль не реализует собственный HTTP-клиент к Anthropic/другому провайдеру, см. integrations-bus).

suggests:пусто. Направление зависимостей строгое (правило 3, module-dependencies): cms/ai-content лежит ниже по графу, чем его потребители, поэтому именно потребители (cms/seo-engine, cms/blog, cms/commerce-catalog и любой будущий контентный модуль) объявляют cms/ai-content в своём suggests и обращаются к AiContentService/ставят задачу в очередь ai-content сервис-вызовом (канал 4). Модуль не содержит ни одной строки кода, знающей о существовании блога, каталога или SEO-аудита — обратная связь описана в «Таблице взаимодействий» ниже, по образцу integrations-bus.

Provides: не регистрирует канонический contract — точечный сервис AiContentService + события, не кросс-доменный интерфейс из реестра §2 стандарта (новый контракт — только ревизией ядра, здесь в этом нет необходимости: у генерации ровно один вид реализации).

Поведение при выключении: у модулей-потребителей действие «ИИ: сгенерировать» исчезает/дизейблится с подсказкой «генерация недоступна — включите модуль ИИ-контента»; ручной ввод контента продолжает работать как обычно — деградация удобства, не функции сайта. История уже сгенерированных и принятых черновиков не удаляется (она уже стала обычным контентом потребителя). При выключении/недоступности cms/integrations-bus модуль переходит в отложенный режим: постановка новой задачи отклоняется с понятной ошибкой («интеграции временно недоступны»), существующие данные не теряются.

Стоимость внешних API: провайдер (Anthropic и опциональный fallback) тарифицирует по токенам — расход виден в дашборде модуля (см. «Компоненты»), не только факт сбоя; дневной лимит запросов и дневной бюджет — обязательные настройки (см. «Настройки»).

Модель данных

ТаблицаКлючевые поляПримечание
cms_ai_content_jobsid (ULID), target_type, target_id (nullable), kind enum, prompt_template_id (nullable FK), prompt_snapshot (text), variables (json), status enum(queued|processing|completed|failed), result (json, nullable), tokens_used (int, nullable), cost_minor (int, nullable) + currency_code, error (text, nullable), created_by (FK users), lock_versionодна задача генерации = один черновик-кандидат
cms_ai_content_promptsid, kind enum, name, template (text), variables_schema (json), is_active (bool), is_default (bool), created_by, lock_versionдекларативные шаблоны промптов по видам генерации

target_type/target_id — полиморфная nullable-ссылка на сущность потребителя (nullable специально: допускается «черновая» генерация ещё не сохранённой сущности, например текста будущей статьи до её создания); модуль не имеет FK на чужие таблицы (правило §4 стандарта — «чужие таблицы только чтение через сервисы владельца», здесь даже чтения нет, только строковый идентификатор для последующего сопоставления потребителем). status/kind — PHP Enum; variables/result/variables_schema — JSONB с касом 'array'; составной индекс (target_type, target_id) — быстрая выборка «какие задачи были для этой сущности»; индекс по status — очередь/дашборд обработки; cms_ai_content_jobs как журнальная таблица — кандидат на BRIN по created_at при росте; частичный уникальный индекс (kind, is_default) WHERE is_default = true на cms_ai_content_prompts — ровно один дефолтный шаблон на вид генерации. lock_version — optimistic lock (§4 стандарта): на cms_ai_content_prompts защищает от одновременной правки шаблона двумя админами; на cms_ai_content_jobs защищает от гонки «воркер очереди завершает задачу одновременно с ручным отклонением её админом» (см. «Крайние случаи»).

ПДн-паспорт: модуль в штатных видах генерации (seo_meta, seo_text, product_description, article, excerpt) работает с редакционным контентом сайта (тексты страниц/товаров/статей), не с персональными данными посетителей — декларация «ПДн посетителей не храню». Единственный риск — редактор вручную впишет в свободные переменные промпта персональные данные (например, вставит текст с контактами клиента); модуль это не запрещает архитектурно (переменные — обычный пользовательский ввод), поэтому cms_ai_content_jobs (включая prompt_snapshot) хранится ограниченный срок — ai-content.job_retention_days (см. «Настройки»), после которого запись очищается командой cms:ai-content:purge-stale. created_by — ссылка на учётную запись сотрудника (не посетителя), для аудита авторства генерации, а не ПДн-объект в смысле 152-ФЗ.

Входные и выходные данные

Входы:

ИсточникПоляЧем валидируется
Filament-action «ИИ: сгенерировать» у потребителя → POST /api/v1/admin/ai-content/jobskind, target_type, target_id (nullable), variablesFormRequest-whitelist; kind — существующий enum; variables сверяются с variables_schema активного шаблона этого kind (лишние/неописанные ключи отклоняются); Idempotency-Key обязателен (внешний платный эффект, §7 стандарта)
CRUD шаблонов промптов (Filament//api/v1/admin/ai-content/prompts)kind, name, template, variables_schema, is_active, is_default, lock_versionFormRequest; template — только простая подстановка по объявленному в variables_schema списку, без исполняемого кода (защита от инъекции через сам шаблон, см. «Безопасность»); lock_version сверяется — иначе 409
Отклонение черновика (POST /api/v1/admin/ai-content/jobs/{id}/discard)id (путь)permission ai-content.generate, задача обязана принадлежать статусу completed/failed (нельзя отклонить ещё выполняющуюся)
Ответ провайдера (через коннектор integrations-bus)JSON-блок по ожидаемой для kind структуреконнектор классифицирует ответ (успех/временная ошибка/постоянная ошибка) до возврата в модуль; JSON-блок разбирается со схемной проверкой (см. «Крайние случаи»)
Admin API: фильтры списка задач (GET .../jobs)filter[kind], filter[status], filter[target_type], sort, cursorFormRequest whitelist (конвенция API ядра)

Выходы:

ПотребительДанныеФормат
Filament-модалка потребителяпредпросмотр промпта (до отправки), предпросмотр результата (до принятия)props модалки, поля — по kind
Модуль-потребитель (после Accept в UI)сгенерированные поля черновикаJSON по схеме kind, потребитель сам решает, куда и как записать в свою сущность
Шина событийAiContentGenerated / AiContentGenerationFailed / AiContentQuotaExceededpayload → подписчики (потребитель, cms/audit, cms/health)
Admin API (GET .../jobs/{id}, GET .../jobs)статус/результат задачи, список с фильтрамиконверт {data, meta}, keyset-пагинация
Filament-дашборд расходатокены/стоимость по дню/kind, остаток дневного лимита и бюджетатаблица/график в ресурсе

Всё, что не перечислено как вход, модуль отвергает (whitelist-принцип §11 стандарта) — в частности, variables вне variables_schema активного шаблона не подставляются в промпт, а отклоняют запрос ещё до постановки задачи в очередь.

Настройки (группа ai-content)

КлючТипДефолтaffectsPageCacheОписание
ai-content.providerstringanthropicнетОсновной провайдер (код коннектора integrations-bus)
ai-content.modelstringиз config('ai-content.default_model')нетИдентификатор модели провайдера; конкретный id не хардкодится в ТЗ — сверяется с актуальной документацией провайдера на момент реализации, дефолт живёт в config/ai-content.php
ai-content.max_tokensint2000нетМаксимум токенов ответа на запрос
ai-content.temperaturefloat0.4нетТемпература генерации (детерминированность vs креативность)
ai-content.daily_request_limitint50нетМаксимум задач генерации в сутки (явная квота §6 стандарта)
ai-content.daily_budget_minorint500000 (5000 ₽ в minor units) + currency_codeнетДневной бюджет расхода на генерацию
ai-content.require_reviewbool (заблокировано true)trueнетАрхитектурный инвариант «черновик не публикуется автоматически» — отображается в настройках как зафиксированное значение (не переключается штатным UI), чтобы админ видел гарантию, не как обычный тумблер
ai-content.timeout_secondsint30нетТаймаут одного вызова провайдера через коннектор шины
ai-content.job_retention_daysint90нетХранение истории cms_ai_content_jobs/снимков промптов до очистки (см. «ПДн-паспорт»)
ai-content.fallback_enabledboolfalseнетВне MVP (ревизия 15.07.2026, см. шапку): включает переключение на резервного провайдера при сбое основного; на MVP провайдер только Claude, настройка объявлена в ТЗ, но не включается
ai-content.fallback_providerstring, nullablenullнетВне MVP: код резервного коннектора integrations-bus, используется только при fallback_enabled = true (мульти-провайдерность — отдельная задача после MVP)
ai-content.kill_switchboolfalseнетАварийная остановка всей генерации без выключения модуля

Секреты (API-ключи провайдера и fallback-провайдера) — только .envconfig/ai-content.php (ANTHROPIC_API_KEY и т.п.), в settings-store не попадают (§6 стандарта, «секреты в настройках запрещены»); credentials_ref коннектора в integrations-bus ссылается именно на эти переменные.

API

Публичного неймспейса /api/v1/ai-content/… нет — генерация исключительно административная функция.

МетодПутьДоступНазначение
POST/api/v1/admin/ai-content/jobsadmin (ai-content.generate)Поставить задачу генерации (обязателен Idempotency-Key)
GET/api/v1/admin/ai-content/jobsadmin (ai-content.view)Список задач с фильтрами (keyset)
GET/api/v1/admin/ai-content/jobs/{id}admin (ai-content.view)Статус/результат задачи (для поллинга модалки)
POST/api/v1/admin/ai-content/jobs/{id}/discardadmin (ai-content.generate)Отклонить черновик (не принимать)
GET/POST/PUT/DELETE/api/v1/admin/ai-content/prompts…admin (ai-content.manage)CRUD шаблонов промптов
GET/api/v1/admin/ai-content/usageadmin (ai-content.view)Расход токенов/стоимости, остаток квот

Мутации шаблонов принимают lock_version — рассинхрон отдаёт 409 (конвенции ядра §7 стандарта). Конверт ошибок — общий формат ядра, коды quota_exceeded, kill_switch_active, bus_unavailable, invalid_variables.

Компоненты

Filament: action «ИИ: сгенерировать» — переиспользуемый трейт/компонент, который подключают ресурсы потребителей (cms/blog, cms/seo-engine, cms/commerce-catalog), а не сам модуль встраивается в их ресурсы (направление зависимостей). Модалка в два шага: (1) предпросмотр промпта с уже подставленными переменными и кнопка «Отправить»; (2) после завершения задачи — предпросмотр результата с «Принять» (заполняет форму потребителя, сохранение — штатной кнопкой потребителя) и «Отклонить» (discard). Страница настроек ИИ (провайдер/модель/лимиты/kill-switch/fallback). Ресурс шаблонов промптов (только ai-content.manage). Дашборд расхода: токены/стоимость по дню и виду генерации, остаток дневного лимита и бюджета, статус kill-switch. Команды: cms:ai-content:usage-report --json, cms:ai-content:purge-stale --json (чистит задачи старше job_retention_days и зависшие в processing дольше таймаута — переводит в failed с error_type=stale_timeout).

Демо-контент: у модуля нет собственных блоков/виджетов (только Filament-компоненты) — как и integrations-bus, полноценный сидер демо-данных не применим; для скриншотов галереи/playground допустим необязательный сидер с 2-3 фиктивными завершёнными задачами (разных kind) для показа модалки предпросмотра результата без реального вызова провайдера.

События и обмен

СобытиеКогдаPayload
AiContentGeneratedзадача успешно завершена, черновик готов к просмотруjob_id, kind, target_type, target_id, tokens_used, cost_minor
AiContentGenerationFailedзадача завершилась ошибкойjob_id, kind, error_type
AiContentQuotaExceededзапрос отклонён до постановки в очередь (лимит/бюджет исчерпан)kind, requested_by, limit_type

Слушает: —. FilterBus не использует. Provides-контрактов не реализует (см. «Зависимости и выключение»).

Таблица взаимодействий (обратная связь описана стилем integrations-bus — модуль не требует потребителей, они требуют его):

Сущность/модульКаналНаправлениеЧто происходит
cms/blog (объявляет ai-content в своём suggests)сервис-вызов (канал 4)inставит задачу kind=article/excerpt для черновика статьи, слушает AiContentGenerated, чтобы предложить редактору принять черновик
cms/seo-engine (объявляет ai-content в своём suggests)сервис-вызов (канал 4)inпредлагает kind=seo_meta/seo_text для страниц, найденных аудитом как тонкий контент/дублирующиеся мета
cms/commerce-catalog (объявляет ai-content в своём suggests)сервис-вызов (канал 4)inставит задачу kind=product_description для карточки товара/категории
cms/integrations-busrequires (канал 4)outвсе HTTP-вызовы к провайдеру ИИ (основному и fallback) идут только через коннектор шины
Очередь ai-contentканал 5out (внутри себя)асинхронная обработка задач генерации
cms/audit (если включён)событие (канал 1)outфиксирует постановку/принятие/отклонение черновика, правки шаблонов промптов, переключение kill-switch
cms/healthсобытие (канал 1)outагрегирует AiContentGenerationFailed/AiContentQuotaExceeded и отставание очереди ai-content в общий health-отчёт
Ядро (SettingsStore)сервис-вызов контракта ядраinчтение своей группы настроек ai-content (0 запросов на горячем пути, кеш группы)

Фоновая работа

Очередь ai-content — джоба GenerateAiContent: читает снимок промпта и переменные, вызывает AiContentService, который через integrations-bus обращается к коннектору провайдера (основному, при сбое и fallback_enabled — к резервному); ретраи с backoff, circuit breaker и rate-limit — на уровне шины, модуль их не дублирует. Джоба идемпотентна: повторная обработка того же job_id (например, после сбоя воркера между HTTP-ответом и записью результата) не создаёт второй черновик — перед записью проверяется текущий status задачи (запись только из queued/processing, повторное завершение завершённой задачи — no-op). Расписание (ScheduleRegistrar): cms:ai-content:purge-stale ежесуточно — переводит зависшие в processing дольше ai-content.timeout_seconds × запас в failed (error_type=stale_timeout) и удаляет записи старше ai-content.job_retention_days.

Производительность и кеш

Генерация — не горячий путь публичного сайта (только административное действие), требований к бюджету page-cache нет. Горячий путь модуля — постановка задачи и чтение статуса при поллинге модалки: сверка daily_request_limit/daily_budget_minor — один агрегирующий запрос (сумма за текущие сутки по индексу created_at+status), не N+1 по задачам. Список задач в админке — keyset-пагинация. Шаблоны промптов кешируются по тегу ai-content:prompts (меняются редко — правка в Filament), инвалидация на afterSave() ресурса. Дашборд расхода — один агрегирующий запрос по дню/kind, не по каждой задаче.

Критичные индексы: составной (target_type, target_id) на cms_ai_content_jobs (поиск задач по сущности), индекс status (очередь/дашборд), частичный уникальный (kind, is_default) на cms_ai_content_prompts, BRIN по created_at на cms_ai_content_jobs при росте объёма.

Безопасность

Границы входа: FormRequest-whitelist на постановку задачи (variables сверяются со variables_schema активного шаблона — лишние ключи отклоняются до подстановки в промпт) и на CRUD шаблонов. Шаблон промпта — не исполняемый код: подстановка переменных — простая замена на строковое значение, не Blade/PHP eval и не произвольный шаблонизатор с логикой — это исключает RCE через сам промпт-шаблон (в отличие от доверенного HTML {!! !!}, здесь никакой шаблонизации с исполнением не допускается вовсе). Редактирование шаблонов промптов — только ai-content.manage (studio/admin), обычный редактор ставит задачи, но не переопределяет системный промпт — иначе открывается вектор prompt injection через изменение самого шаблона, а не только пользовательских переменных.

Результат генерации перед использованием потребителем как rich-text контент проходит те же стандартизированные санитайзеры ядра (Html::sanitize), что и ручной ввод — вывод модели не считается доверенным HTML ни при каких условиях (§11 стандарта, §4 правил проекта «фильтрация ввода»).

Секреты: API-ключи — только .envconfig/ai-content.php; в cms_ai_content_jobs (включая prompt_snapshot, result, error) ключи и токены провайдера попасть не могут архитектурно — переменные шаблона формируются из полей контента и variables_schema, не из конфигурации коннектора. Rate-limit и квоты — daily_request_limit/ daily_budget_minor: достижение — человекочитаемая ошибка и метрика, не 500 и не тихое обрезание (§6 стандарта). Kill-switch — только ai-content.manage (по факту — только studio-роль, см. матрицу).

Матрица ролей:

Действиеadminменеджерредакторstudio
Поставить задачу генерации / принять / отклонить черновик
Просмотр дашборда расхода (токены/стоимость)
CRUD шаблонов промптов
Настройки провайдера/модели/лимитов/fallback
ai-content.kill_switch (аварийное отключение)

Права: ai-content.generate, ai-content.view, ai-content.manage.

UX-требования

Админ:

  • Пустой список задач — не пустая таблица, а подсказка «генераций ещё не было — нажмите „ИИ: сгенерировать“ на странице контента».
  • Модалка предпросмотра промпта показывает итоговый текст с подставленными переменными до отправки — редактор видит, что именно уйдёт во внешний API, может отменить.
  • Пока задача в queued/processing — модалка показывает индикатор ожидания (поллинг статуса), не блокирует остальную работу с формой потребителя.
  • Ошибки — человеческим языком: «Дневной лимит запросов исчерпан (50/50), сброс в 00:00» вместо «429»; «ИИ временно недоступен, интеграции не отвечают — попробуйте позже» вместо стектрейса; «Модель вернула нераспознанный ответ — попробуйте ещё раз или обратитесь к разработчику» при invalid_response.
  • Массовое действие: постановка генерации product_description сразу для нескольких выбранных товаров в списке потребителя (каждый — отдельная независимая задача/черновик, не групповая транзакция).
  • Отклонение черновика с уже показанным предпросмотром — без дополнительного подтверждения (черновик и так не сохранён ни в чём, кроме журнала задач).
  • Правка активного шаблона промпта другим админом одновременно — конфликт lock_version: «Шаблон изменён другим пользователем, обновите страницу».

Посетитель: модуль не имеет публичного UI — посетитель никогда не взаимодействует с cms/ai-content напрямую, только косвенно получает итоговый (уже проверенный человеком) контент через модуль-потребитель.

Крайние случаи и типовые баги

  • Превышение дневного лимита/бюджета — запрос отклоняется до постановки в очередь человекочитаемой ошибкой + метрикой (AiContentQuotaExceeded), не 500 и не молчаливая постановка сверх лимита.
  • Таймаут провайдера — задача переходит в failed с error_type=timeout, черновик не создаётся; при fallback_enabled — одна попытка через резервный коннектор перед финальным failed.
  • Включён ai-content.kill_switch — новая постановка задачи отклоняется сразу (действие в UI потребителя задизейблено с подсказкой), задачи, уже стоящие в очереди — переводятся в failed (error_type=kill_switch), не выполняются; выключение kill-switch не переигрывает их автоматически.
  • cms/integrations-bus недоступен/выключен — постановка задачи отклоняется с error_type=bus_unavailable и понятным сообщением; данные не теряются, повторная попытка — вручную после восстановления шины.
  • Повтор обработки джобы (at-least-once очереди) — идемпотентно: запись результата только из статусов queued/processing, повторное завершение уже completed/failed задачи — no-op, второй черновик не создаётся.
  • Модель вернула невалидный JSON-блок — безопасный разбор со схемной проверкой (structured-output вместо парсинга регексами, см. opensource/ai-llm.md); при несовпадении схемы — failed с error_type=invalid_response, воркер не падает, остальная очередь обрабатывается штатно.
  • Гонка «воркер завершает задачу одновременно с ручным discard»lock_version на cms_ai_content_jobs: одна из двух операций получает конфликт и обрабатывается как «задача уже в терминальном статусе», не перезаписывает результат молча.
  • Одновременная генерация для одной и той же сущности двумя редакторами — не запрещена и не дедуплицируется: каждая постановка создаёт независимую задачу/черновик, оба видны потребителю, редактор сам выбирает, какой принять (проще и безопаснее, чем угадывать «дублирующий» запрос по неточному совпадению переменных).
  • Двойной клик «Сгенерировать»Idempotency-Key на POST /jobs не даёт создать вторую задачу с тем же ключом в окне идемпотентности.
  • Целевой контент длиннее контекстного окна модели (например, огромная статья для kind=article) — переменная обрезается по конфигурируемому лимиту с явной пометкой в промпте «текст сокращён»; редактор видит в предпросмотре, что вход был усечён, и может решить, достаточно ли этого для черновика.
  • Шаблон промпта, на который ссылается задача, удалён/деактивирован после постановки — не ломает уже поставленную задачу: используется prompt_snapshot, зафиксированный в момент создания, а не живая ссылка на cms_ai_content_prompts.
  • У вида генерации (kind) нет активного дефолтного шаблона — self-test при постановке задачи отказывает с понятной ошибкой «для этого вида генерации не настроен шаблон промпта», не создаёт задачу с пустым/непредсказуемым промптом.
  • Выключен модуль-потребитель (cms/blog/cms/seo-engine/cms/commerce-catalog) — не касается cms/ai-content: он продолжает работать (задачи можно ставить через admin API/дашборд), просто у выключенного потребителя не отображается сама кнопка «сгенерировать» — это его собственная деградация, не ответственность ai-content.

Донорский код

Что взятьПуть
Модалка предпросмотра промпта/результата, парсинг структурированного JSON-ответа модели, авто-расчёт excerpt/reading_time, логика fallback-провайдераuniversal/artel, ClaudeArticleGenerator (точный путь в репозитории донора — сверить при реализации, в этом ТЗ не выдан)

Legacy-импорт: не применимо — история ИИ-генераций специфична для нового модуля, у донорских сайтов нет сопоставимой структуры данных для маппинга (аналогично integrations-bus: переносить с прежней платформы нечего, чистый журнал задач при подключении модуля).

Тесты и приёмка

  • [ ] Контрактный тест: постановка задачи проходит только через AiContentServiceintegrations-bus, без прямого HTTP к провайдеру из модуля
  • [ ] variables вне variables_schema активного шаблона отклоняются до подстановки в промпт
  • [ ] При выключении модуля/недоступности integrations-bus потребители не падают, кнопка генерации недоступна с понятным сообщением
  • [ ] ai-content.daily_request_limit/daily_budget_minor — превышение отклоняет запрос человеческой ошибкой + событием AiContentQuotaExceeded, не создаёт задачу
  • [ ] ai-content.kill_switch блокирует новую постановку и переводит уже стоящие в очереди задачи в failed (error_type=kill_switch)
  • [ ] Невалидный JSON-ответ модели не роняет воркер очереди, задача уходит в failed (error_type=invalid_response), остальная очередь обрабатывается
  • [ ] Повторная обработка джобы (at-least-once) не создаёт второй черновик для того же job_id
  • [ ] Конкурентная гонка «завершение воркером» vs «ручной discard» разрешается через lock_version, без молчаливой перезаписи результата
  • [ ] Конкурентная правка шаблона промпта двумя админами → 409 по lock_version
  • [ ] Результат генерации перед сохранением как rich-text проходит Html::sanitize, как и ручной ввод
  • [ ] Права ai-content.generate/.view/.manage разграничены; матрица ролей покрыта тестом
  • [ ] prompt_snapshot задачи не содержит секретов/ключей провайдера (тест на утечку)
  • [ ] Нет N+1 в дашборде расхода и в проверке дневных квот (агрегирующий запрос)
  • [ ] cms:ai-content:purge-stale переводит зависшие processing-задачи в failed и удаляет записи старше ai-content.job_retention_days, идемпотентен при повторном запуске
  • [ ] Контрактный набор cms-testing зелёный, пакет тестируется в testbench-изоляции
  • [ ] Feature-тест на каждый роут (постановка задачи, статус, discard, CRUD шаблонов, usage)
  • [ ] Тестовая БД только ai_content_test; migrate:fresh/refresh/reset/db:wipe запрещены

Внутренняя база знаний студии