# Статья, портфолио и услуга с готовыми фотографиями

В комплект уже входят **два JPEG и три готовых примера**. Ничего искать или дорисовывать не нужно. Каждый пример содержит превью, галерею, отдельные подписи и alt. Материалы помечены «Демо»: это изображения из интерфейса Аишки, а не реальная работа для клиента.

![Демо: светлая керамическая ваза на нейтральном бежевом фоне](https://eval.su/images/ceramic-vase-studio.jpg)

![Демо: светлая ваза с оливковой веткой на деревянной тумбе](https://eval.su/images/ceramic-vase-interior.jpg)

- **Статья — `article.example.json`.** Обложка, вступление, форматированный текст, иллюстрация, галерея, список и код с указанием языка.
- **Портфолио — `portfolio.example.json`.** Задача, этапы, две композиции в галерее и результат демонстрации. Без выдуманных дат, клиентов и показателей.
- **Услуга — `service.example.json`.** Визуальные примеры, состав работ, этапы, ожидаемый результат и ограничения. Без выдуманной цены или срока.

## Попробовать сразу, без ключа

Скачайте [готовый ZIP](https://eval.su/docs/examples/media.zip), распакуйте и перейдите в папку комплекта:

```sh
cd examples/integrations/media
node publication-draft.mjs article.example.json article.state.json
node publication-draft.mjs portfolio.example.json portfolio.state.json
node publication-draft.mjs service.example.json service.state.json
```

Нужен Node.js 22+. Команды читают **вложенные фотографии**, проверяют подписи и ссылки и показывают JSON будущих черновиков. Они не отправляют запросы на сайт, не создают журнал и не вызывают OpenAI. `READY_FILE_UUID_*` в результате — обозначения места для UUID; отправлять их серверу нельзя.

## Сохранить выбранный пример в свой профиль

В [настройках интеграций](https://eval.su/settings/integrations/) выберите профиль и разрешения HTTP API: `profiles:read`, `files:read`, `files:write`, затем разрешения выбранного материала: `articles:read/write`, `portfolio:read/write` или `services:read/write`.

Передайте токен через защищённое окружение `AISHKA_ACCESS_TOKEN`. Не вставляйте его в JSON, командную строку или промпт. `AISHKA_ORIGIN` по умолчанию равен `https://eval.su`.

Выполните **одну** подходящую команду:

```sh
node publication-draft.mjs article.example.json article.state.json --save-draft
```

Для портфолио или услуги замените оба имени на `portfolio.example.json portfolio.state.json` либо `service.example.json service.state.json`. У каждого материала свой журнал. Программа загрузит фотографии, получит UUID готовых файлов и сохранит приватный черновик. Ответ содержит `webUrl`: откройте его и проверьте картинки, подписи и текст. Пример не отправляет материал на модерацию и не публикует его.

## Заменить демонстрацию своей работой

1. Скопируйте выбранный `*.example.json` в рабочую папку и положите рядом свои фотографии.
2. В `media[].path` укажите путь к каждому файлу относительно JSON. В `caption` напишите контекст кадра, в `alt` — что на нём видно. Проверьте права на материалы.
3. Замените название и текст фактическим описанием работы. Уберите «Демо» только после замены демонстрационных материалов. Не придумывайте даты, размеры, клиентов и результаты.
4. Сохраните соответствие ссылок: обложка `content.article.coverId`, превью `content.preview.assetIds`, изображения и галереи `blocks[].mediaIds` используют ключи из `media[].key`.
5. Запустите локальную проверку без `--save-draft`, затем сохранение с **новым** журналом для нового материала. Старый журнал нужен, чтобы безопасно повторить прежнюю операцию.

Для статьи с собственного сайта используйте [импорт HTML и изображений](https://eval.su/docs/examples/site-import.md). Если текст и структуру готовит ИИ, передайте ему JSON примера без ключа: приложение загружает локальные файлы и подставляет UUID самостоятельно. [Адаптер OpenAI](https://eval.su/docs/examples/openai.md) — отдельный рецепт; байты фотографий и ключ не передаются модели этим скриптом.

## Что делает программа

Для каждой фотографии она вычисляет SHA-256 и вызывает `POST /api/v1/files/uploads` с `requestUuid`, `name`, `mimeType`, `size`, `sha256`, `purpose`. Далее передаёт исходные байты через `PUT uploadUrl` с `application/octet-stream`, завершает загрузку через `POST /api/v1/files/uploads/{uuid}/complete` и проверяет `GET /api/v1/files/{uuid}`. В публикацию попадёт только файл со статусом `ready`, совпадающими размером и контрольной суммой.

Ссылки `@cover`, `@detail` существуют только в локальном манифесте. Программа заменяет их на реальные UUID и добавляет `content.assets`: `id`, `kind: image`, `caption`, `alt`. Имя, MIME, размер и URL достраивает сервер. Обложка статьи — `content.article.coverId`, превью списка — `content.preview.assetIds`, иллюстрации и галерея — `blocks[].mediaIds`. Один файл может использоваться в нескольких блоках без повторной загрузки.

JSON показывает форматированный текст (`**жирный**`, `*курсив*`, встроенный код, HTTP(S)-ссылки, заголовки и маркированный список), отдельные блоки `image`, `gallery`, `list`, `code`. Это поддерживаемый поднабор разметки, не полный Markdown и не HTML. Для кода используйте `type: code`, `language`, `text`; не вставляйте HTML в `richtext`. `publicationData()` заполняет остальные поля каждого блока, необходимые редактору.

Фото: JPEG, PNG, WebP, GIF, AVIF, SVG; до 25 МиБ на файл, 40 файлов и 300 МиБ на материал. Сервер проверяет сигнатуру и безопасность SVG. Истёкшая или отклонённая загрузка требует разбора причины; программа не создаёт замену молча. Ссылки и перенаправления на другой origin отклоняются до передачи ключа.

## Повтор после ошибки

Журнал сохраняет UUID команд **до** изменения сервера и результат сохранения — до контрольного чтения. Повторяйте ту же команду с тем же JSON, фотографиями и `*.state.json`. Это восстанавливает работу после неизвестного исхода загрузки или сохранения без нового черновика. После успешного создания повтор только читает актуальную публикацию и не перезаписывает ручные правки.

Журнал привязан к origin, пользователю, профилю и контрольной сумме содержимого, имеет права `0600`, ключ не содержит. Он нужен для восстановления; не удаляйте и не публикуйте его. Новый материал — новый журнал. Одновременный запуск блокируется `.lock`; после аварийного завершения сначала убедитесь, что процесс из файла остановлен, затем удалите только устаревший `.lock`. Ответы `409`, `429`, ошибки прав или файлов программа показывает и останавливается; после выяснения причины продолжите с тем же журналом. При `429` соблюдайте `Retry-After`.

## Портфолио и услуги

Готовые `portfolio.example.json` и `service.example.json` уже содержат нужные структуры. В портфолио `data.result` описывает результат работы; известную дату можно добавить отдельно в `data.caseReleasedAt: YYYY-MM-DD`. В услуге `data.result` — ожидаемый результат, а `data.service` содержит этапы, состав результата и ограничения. Оба примера используют `content` без секции `content.article`. Вместо `articles:read/write` нужны `portfolio:read/write` или `services:read/write`.

Файл принадлежит выбранному профилю и назначению: загруженный с `purpose: article` UUID нельзя прикрепить к услуге. Программа выставляет назначение по `kind`. Для другого назначения нужен отдельный запуск и журнал. Цены, сроки, даты, авторы и достижения не подставляются автоматически.

Изменение уже существующей публикации не входит в этот пример: сначала получите полное `draft` и `revision` через `GET /api/v1/me/publications/{uuid}`; сохраните **полную** изменённую структуру через `PATCH /api/v1/publications/{uuid}` с `publicationUuid`, актуальной `expectedRevision`, `kind` и новым постоянным `requestUuid`. `data` заменяет черновик целиком, это не частичный merge. Не переносите в тело `approved`, `status`, `ownerUuid` или поля ответа.

Полный HTTP/MCP справочник: [файлы и публикации](https://eval.su/docs/examples/media-publications.md). Работа с OpenAI — [отдельный адаптер](https://eval.su/docs/examples/openai.md) и [его готовые файлы](https://eval.su/docs/examples/openai.zip); этот скрипт не утверждает, что библиотека файлов ChatGPT автоматически доступна приложению.

## Проверка

```sh
npm ci
npm test
```

Эти проверки полностью включены в ZIP и не требуют исходников сайта. Проверки загружают все три готовых манифеста с вложенными JPEG, сверяют точные байты и SHA-256, обложку, превью, галереи и подписи. Имитатор транспорта проверяет готовность, потери ответа на каждом шаге, изоляцию пользователя и профиля, сохранение ручных правок после повтора, ошибки и недопустимые URL.

Для разработчиков Аишки дополнительно существует проверка настоящего React-рендерера: `node --test examples/integrations/media/publication-render.test.cjs` из корня полного checkout после установки его зависимостей. Она проверяет галерею, заголовки, выделение текста и подсветку кода и выполняется в CI; это не команда автономного ZIP.

`TestMediaCookbookHTTPUploadDraftAndRepeat` в `apps/api/internal/publications/media_cookbook_integration_test.go` запускает именно этот CLI против настоящих HTTP-обработчиков, PostgreSQL 18 и временного файлового хранилища. Он проверяет загрузку JPEG, точные байты скачивания, приватность, подписи, обложку, запрет чужого назначения, повтор без дублей и сохранность ручной правки. Для запуска нужен `TEST_DATABASE_URL` отдельной базы с префиксом `aishka_test` и `go test -tags=integration ./internal/publications` из `apps/api`. Проверки не загружают файлы в eval.su и не вызывают модель.
