Техническая справка: AI-генерация меты
Документ для разработчиков. Описывает устройство фичи генерации меты и её применения к
товарам (apply-meta).
Идея
Генерация — это сохранённый переиспользуемый payload тех же групп, что и
копирование между товарами: описание, meta description, теги, характеристики. Поэтому
генерация применяется к товарам через тот же пайплайн apply-meta, что и «копирование
из другого товара». Внешний AI изолирован интерфейсом.
Жизненный цикл асинхронный: POST /api/v1/generations сразу сохраняет строку со
статусом PENDING (входы резолвятся в input_snapshot в этот момент) и после коммита
запускает воркер (generation/GenerationWorker, @Async("aiGenerationExecutor") — свой
пул на 2 потока, чтобы двухминутные job'ы не толкались с пайплайном). Воркер пишет
output + DONE либо ERROR + error_message. Фронт поллит GET /{id}. Превью-режима
«в памяти» нет — каждая генерация сохранена с момента запуска.
generation/StaleGenerationSweeper на старте приложения помечает ERROR'ом PENDING-строки
старше ai.generation.stale-pending-minutes (env AI_GENERATION_STALE_PENDING_MINUTES,
дефолт 10) — их убил перезапуск сервера.
metaTitle не генерируется никогда — он заполняется SEO-шаблоном из app_config;
при применении SEO-группы из генерации фронт передаёт текущий metaTitle товара, чтобы
не затереть его. Контракт для реального AI-клиента: docs/ai/generation-contract.md.
Модель данных
Таблица meta_generations — миграции
back/.../db/changelog/changes/084-create-meta-generations.xml и
085-add-generation-error-message.xml (зарегистрированы в db.changelog-master.xml).
Сущность entity/MetaGenerationEntity.java:
| Поле | Тип | Назначение |
|---|---|---|
title | varchar | Автозаголовок AI · {товар}; переименовывается в таблице генераций |
status | varchar(20) | PENDING (воркер работает) → DONE | ERROR |
errorMessage | text | Причина при ERROR; чистится при regenerate |
model | varchar(100) | Выбранная модель (значение с фронта; логики маршрутизации нет) |
comment | text | Доп. инструкция для AI |
sourceProductId | bigint FK | Товар-основа; FK ON DELETE SET NULL |
genDescription/genSeo/genTags/genAttributes | boolean | Что было запрошено |
inputSnapshot | jsonb (Map<String,String>) | Резолвленные значения товара, переданные AI (метка → значение) |
output | jsonb (GenerationOutput) | Результат |
model/generation/GenerationOutput.java (JSONB): descriptionDoc (DescriptionDoc),
metaDescription, tagNames: List<String> (теги — имена, не id),
attributes: List<GenerationAttribute(name,value)>. Поля metaTitle нет.
JSONB маппится через @JdbcTypeCode(SqlTypes.JSON) (как descriptionDoc у товара).
Граница внешнего AI
generation/MetaGenerationClient.java— интерфейс:GenerationOutput generate(GenerationCommand)List<String> models()(алиасы для выпадающего списка в UI).
generation/GenerationCommand.java— вход:model, comment, description, seo, tags, attributes, inputs: Map<String,String>.generation/StubMetaGenerationClient.java— синхронная заглушка (активна приai.platform.enabled=false, по умолчанию): детерминированный плейсхолдер, заполняет только запрошенные группы, вплетает комментарий.generation/AiPlatformMetaGenerationClient.java— реальный клиент MBC AI Platform (активен приai.platform.enabled=true; см.ai-platform-integration.md). Создаёт job на каналеseo_product_meta(POST /v1/generations?wait=Nс обязательнымIdempotency-Key), при 202 опрашиваетGET /v1/jobs/{id}до терминального статуса;FAILED/REJECTED/таймаут →AiGenerationException(502 вGlobalExceptionHandler). Маппинг входов — §2.5ai-platform-integration.md(Название → name,Характеристики → specsи т.д.).models()читает каталогGET /v1/channelsс кэшем 5 минут.generation/platform/—AiPlatformProperties(@ConfigurationProperties("ai.platform"):enabled, baseUrl, apiKey, channel, waitSeconds, pollIntervalMs, pollTimeoutMs; env-переменныеAI_PLATFORM_*),AiPlatformConfig(бинaiPlatformRestClientс заголовкомX-Api-Key), DTO ответа платформы.
Платформа сама приводит result к схеме generation-contract.md (§2.2 интеграционного
документа), поэтому дополнительной валидации в клиенте нет — результат десериализуется
прямо в GenerationOutput.
Сервис и эндпоинты
service/MetaGenerationService + service/impl/MetaGenerationServiceImpl:
start(StartGenerationRequest)— грузит товар,buildInputs()собирает выбранные поля вMap<метка,значение>, сохраняет PENDING-строку и после коммита (черезTransactionSynchronization.afterCommit) запускаетGenerationWorker.run(id).list(productId)— все генерации или только по товару-основе;sourceProductNameрезолвится батчем для отображения/поиска.get / update (только title) / delete / regenerate.regenerateвозвращает строку в PENDING (ошибка чистится, старыйoutputживёт до перезаписи) и заново запускает воркер; для уже PENDING — no-op.buildInputs()— маппинг ключей полей (name, category, brand, country, price, description, attributes, tags) в русские метки (Название, Категория, Бренд, Страна, Цена, Текущее описание, Характеристики, Теги); эти метки читает клиент.
Контроллер controller/MetaGenerationController — /api/v1/generations:
| Метод | Путь | Назначение |
|---|---|---|
| POST | / | Запуск: сохраняет PENDING и возвращает её (201); результат — поллингом |
| GET | /?productId= | Список (по createdAt desc), опционально по товару-основе |
| GET | /models | Алиасы моделей (каталог платформы или заглушка) |
| GET | /{id} | Одна генерация (этим же поллится статус) |
| PUT | /{id} | Переименование |
| DELETE | /{id} | Удалить (204) |
| POST | /{id}/regenerate | Перезапуск из сохранённого inputSnapshot |
Доступ: /api/v1/generations/** → ADMIN, SUPER_ADMIN (в config/SecurityConfig).
Исключение GenerationNotFoundException зарегистрировано в GlobalExceptionHandler.
Применение: apply-meta
POST /api/v1/products/{id}/apply-meta (controller/ProductController), DTO
dto/product/ApplyMetaRequest:
applyDescription + descriptionDoc
applySeo + metaTitle + metaDescription
applyTags + tagIds
applyAttributes + attributes
Каждый флаг applyX включает свою группу (false = не трогать, true = применить, в т.ч.
пусто). ProductServiceImpl.applyMeta(...) — один @Transactional: описание/SEO
выставляются, теги и MANUAL-характеристики полностью замещаются. Общая логика вынесена в
приватные replaceTags(...) / replaceManualAttributes(...), которые переиспользуют
setTags и setManualAttributes (валидация неизвестного тега откатывает транзакцию).
apply-meta не знает о генерациях — фронт резолвит генерацию в тот же черновик.
Фронтенд
admin/products/[id]/generate/page.tsx— экран запуска: форма слева («Текущее описание» по умолчанию не выбрано), справа история генераций товара (GET /?productId=). После запуска — анимация ожидания с поллингомGET /{id}: до 60 с каждые 2 с, до 180 с каждые 10 с с таймером «следующее обновление через N с», дальше автополлинг останавливается (кнопка «Проверить ещё раз»).DONE→ автредирект в/copy-from?generationId=;ERROR→ текст ошибки + «Повторить». Открытие страницы с уже идущей генерацией подхватывает её поллинг.admin/generations/page.tsx— таблица всех генераций: клиентский поиск (заголовок / модель / товар-основа), фильтр по статусу,?productId=(предфильтр из истории товара), действия: применить / повторить / переименовать / удалить. Пока в списке есть PENDING — список сам обновляется раз в 5 с.admin/products/[id]/copy-from/page.tsx— мастер. Источник (товар или генерация) приводится к общемуResolvedSource; вкладки источника; резолвtagNames → tagIds(несовпавшие → блок «создать/пропустить» черезPOST /api/v1/tags); поддержка?generationId=(обёрнут вSuspenseиз-заuseSearchParamsв Next 16).components/Admin/DescriptionEditor.tsx— добавлены необязательныеonChange(живой поток значения,onChangeдержится вrefво избежание цикла ре-рендера) иhideSave.- Типы:
types/generation.ts,types/product.ts(ApplyMetaRequest,ProductAttribute).
Тесты
controller/ProductApplyMetaIT— apply-meta (все группы,applyX=false, замена MANUAL с сохранением AUTO, неизвестный тег → 404, 401).controller/MetaGenerationControllerIT— async-цикл (start → поллинг до DONE), фильтрproductId, rename/delete/regenerate, 401, 404. Тестовая транзакция отключена (@Transactional(NOT_SUPPORTED)): поток опирается наafterCommitи отдельный поток воркера, которые не срабатывают внутри откатываемой транзакции; данные чистятся в@AfterEach.generation/StubMetaGenerationClientTest— unit на заглушку.generation/AiPlatformMetaGenerationClientTest— unit на клиент платформы (MockRestServiceServer).
Запуск: cd back && ./gradlew test --tests "*MetaGeneration*" --tests "*ApplyMeta*"
(нужен Docker для Testcontainers).