Тема
ТЗ — Генерация контента ИИ (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_jobs | id (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_prompts | id, 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/jobs | kind, target_type, target_id (nullable), variables | FormRequest-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_version | FormRequest; 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, cursor | FormRequest whitelist (конвенция API ядра) |
Выходы:
| Потребитель | Данные | Формат |
|---|---|---|
| Filament-модалка потребителя | предпросмотр промпта (до отправки), предпросмотр результата (до принятия) | props модалки, поля — по kind |
| Модуль-потребитель (после Accept в UI) | сгенерированные поля черновика | JSON по схеме kind, потребитель сам решает, куда и как записать в свою сущность |
| Шина событий | AiContentGenerated / AiContentGenerationFailed / AiContentQuotaExceeded | payload → подписчики (потребитель, 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.provider | string | anthropic | нет | Основной провайдер (код коннектора integrations-bus) |
ai-content.model | string | из config('ai-content.default_model') | нет | Идентификатор модели провайдера; конкретный id не хардкодится в ТЗ — сверяется с актуальной документацией провайдера на момент реализации, дефолт живёт в config/ai-content.php |
ai-content.max_tokens | int | 2000 | нет | Максимум токенов ответа на запрос |
ai-content.temperature | float | 0.4 | нет | Температура генерации (детерминированность vs креативность) |
ai-content.daily_request_limit | int | 50 | нет | Максимум задач генерации в сутки (явная квота §6 стандарта) |
ai-content.daily_budget_minor | int | 500000 (5000 ₽ в minor units) + currency_code | нет | Дневной бюджет расхода на генерацию |
ai-content.require_review | bool (заблокировано true) | true | нет | Архитектурный инвариант «черновик не публикуется автоматически» — отображается в настройках как зафиксированное значение (не переключается штатным UI), чтобы админ видел гарантию, не как обычный тумблер |
ai-content.timeout_seconds | int | 30 | нет | Таймаут одного вызова провайдера через коннектор шины |
ai-content.job_retention_days | int | 90 | нет | Хранение истории cms_ai_content_jobs/снимков промптов до очистки (см. «ПДн-паспорт») |
ai-content.fallback_enabled | bool | false | нет | Вне MVP (ревизия 15.07.2026, см. шапку): включает переключение на резервного провайдера при сбое основного; на MVP провайдер только Claude, настройка объявлена в ТЗ, но не включается |
ai-content.fallback_provider | string, nullable | null | нет | Вне MVP: код резервного коннектора integrations-bus, используется только при fallback_enabled = true (мульти-провайдерность — отдельная задача после MVP) |
ai-content.kill_switch | bool | false | нет | Аварийная остановка всей генерации без выключения модуля |
Секреты (API-ключи провайдера и fallback-провайдера) — только .env → config/ai-content.php (ANTHROPIC_API_KEY и т.п.), в settings-store не попадают (§6 стандарта, «секреты в настройках запрещены»); credentials_ref коннектора в integrations-bus ссылается именно на эти переменные.
API
Публичного неймспейса /api/v1/ai-content/… нет — генерация исключительно административная функция.
| Метод | Путь | Доступ | Назначение |
|---|---|---|---|
| POST | /api/v1/admin/ai-content/jobs | admin (ai-content.generate) | Поставить задачу генерации (обязателен Idempotency-Key) |
| GET | /api/v1/admin/ai-content/jobs | admin (ai-content.view) | Список задач с фильтрами (keyset) |
| GET | /api/v1/admin/ai-content/jobs/{id} | admin (ai-content.view) | Статус/результат задачи (для поллинга модалки) |
| POST | /api/v1/admin/ai-content/jobs/{id}/discard | admin (ai-content.generate) | Отклонить черновик (не принимать) |
| GET/POST/PUT/DELETE | /api/v1/admin/ai-content/prompts… | admin (ai-content.manage) | CRUD шаблонов промптов |
| GET | /api/v1/admin/ai-content/usage | admin (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-bus | requires (канал 4) | out | все HTTP-вызовы к провайдеру ИИ (основному и fallback) идут только через коннектор шины |
Очередь ai-content | канал 5 | out (внутри себя) | асинхронная обработка задач генерации |
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-ключи — только .env → config/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: переносить с прежней платформы нечего, чистый журнал задач при подключении модуля).
Тесты и приёмка
- [ ] Контрактный тест: постановка задачи проходит только через
AiContentService→integrations-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запрещены