Личный кабинет

Для человека

Правила для ИИ-агента

Чтобы ChatGPT, Cursor или другой ИИ писал материалы в синтаксисе Демонстратора — скачайте пакет правил и отдайте его агенту.

Как пользоваться

В Cursor / Claude Code / VS Code

  1. Распакуйте zip в правила проекта (Cursor Skills, .cursor/rules, AGENTS.md — как принято у вас).
  2. В личном кабинете возьмите API-токен.
  3. Подключите MCP по конфигу ниже.
{
  "mcpServers": {
    "demonstrator-mcp": {
      "url": "https://app.longread.agency/mcp/demonstrator/mcp",
      "headers": {
        "Authorization": "Bearer <токен>"
      }
    }
  }
}
Адрес: /mcp/demonstrator/mcp (не /sse). Без токена подключение не сработает.

В ChatGPT / Gemini / NotebookLM

  1. Загрузите файлы из zip в Knowledge / файлы проекта / вложение чата.
  2. Попросите написать материал.
  3. Скопируйте готовый markdown в редактор Демонстратора.
Без MCP бот сам ничего в ваш аккаунт не запишет.

Дополнительно (не обязательно)

Word/PowerPoint и загрузка медиа с диска — отдельный zip.

Для агента и даты файлов

Primary-источник правил — raw .md на этом origin: начни с SKILL.md и README.md, затем 0106. Вкладки ниже — зеркало. Не меняй план без подтверждения пользователя. Без MCP — отдай готовый markdown, не вызывай API; медиа — CDN-заполнитель или URL от пользователя.

Даты обновления

Сравни updated: в своих копиях с таблицей. Если старше — заново возьми zip или raw .md. Не сравнивай по mtime.

Основной пакет

ФайлОбновлён
README.mdupdated: 2026-08-16 14:20 GMT+3
SKILL.mdupdated: 2026-08-16 14:20 GMT+3
01-syntax.mdupdated: 2026-08-10 17:40 GMT+3
02-methodology.mdupdated: 2026-08-10 17:40 GMT+3
03-mcp-tools.mdupdated: 2026-08-17 13:33 GMT+3
04-css-generation.mdupdated: 2026-08-11 03:40 GMT+3
05-html-widget-rules.mdupdated: 2026-08-10 17:55 GMT+3
06-russian-typography.mdupdated: 2026-08-06 17:35 GMT+3

Помощники (отдельный zip)

ФайлОбновлён
helpers/README.mdupdated: 2026-08-07 11:45 GMT+3
helpers/source-cleanup.mdupdated: 2026-08-15 16:20 GMT+3
helpers/upload-local-media.ps1mtime: 2026-08-15 23:02:27 MSK

Страница собрана: 2026-08-17 13:36:08 MSK

Синтаксис Демонстратора

Материал — один Markdown-файл. Интерактив — блоки :::тип:::/тип.

Жёсткие правила

1. Открытие: :::имя в начале строки. Закрытие: только :::/имя в начале строки. Для каждого открытого блока — своё закрытие. Без исключений. 2. URL и текст тела — со следующей строки после :::имя. На строке :::имя — только имя, вариант и модификаторы. 3. Не выдумывай имена, варианты и модификаторы вне таблицы ниже. 4. %% в начале строки — комментарий. При рендере отбрасывается. 5. GFM-таблицы и автолинки поддерживаются. 6. Блок многострочный: :::имя, пункты ===, тело и :::/имяотдельные строки. Не склеивай в одну строку (:::steps === … === … :::/steps — запрещено). 7. Смысл строки === зависит от блока (см. таблицу ниже). Не копируй паттерн === Заголовок из acc/steps в columns. 8. :::important — короткая врезка (обычно несколько предложений). Не оборачивай в important весь урок, статью или файл с # заголовками и несколькими разделами. 9. Русский текст — по 06-russian-typography.md: тире —, кавычки «ёлочки», многоточие …; не - и не "..." в прозе.

Строка === по типу блока

БлокиСтрока ===Заголовок пункта / колонки
acc, tab/tabs, flip, steps, compare, checklist=== Текст — текст обязателенТекст на той же строке после ===
columnsпустой === — только разделитель колонокЗаголовок колонки — только ### внутри колонки
test=== (часто пустой) или с текстом вопроса — по канону testНе путай с заголовком колонок

Если в превью видны сырые === или обрывки :::/… — сломан разделитель или fence: чаще всего === Заголовок внутри :::columns или незакрытый блок.

Полная таблица блоков

БлокВарианты (после имени)МодификаторыРазмеры на строке :::Закрытие
acc:::/acc
tab / tabs:::/tab
flip:::/flip
test:::/test
stepsgrid:::/steps
checklistduo:::/checklist
compare:::/compare
buttongroup:::/button
importantidea, problem, solution, info, rule:::/important
tmpvideo, sim, dialog, hotspot, hint:::/tmp
embedwide— (размеры в теле):::/embed
audio:::/audio
videowideWxH (напр. 960x540):::/video
file:::/file
web_archivewideWxH:::/web_archive
ai_widgetwideWxH:::/ai_widget
picfontchat, right:::/picfont
sliderduowide:::/slider
hotspotwide:::/hotspot
columnswide:::/columns

Пиши :::columns:::/columns.

:::comments — служебный. Не создавай вручную.

Картинка в обычном Markdown — ![подпись](URL). Не оборачивай в ![…](…) URL внутри блоков, где синтаксис требует голый URL (:::audio / :::video / :::picfont / аватары в :::picfont chat и т.п.). Исключение: кадры :::slider и фон :::hotspot — там как раз ![…](URL).

Вложенность

Заголовки

Только ATX уровней 1–3:

Заголовок H1

Заголовок H2

Заголовок H3

H4–H6 и setext не используй.

Текст

НазначениеСинтаксисЗапрещено
Жирный**текст** или __текст__
Курсив*текст* или _текст_
Код` код `
Ссылка[текст](URL)
Картинка![подпись](URL)[текст](URL) без ! — это ссылка, не картинка

Списки: - пункт или * пункт; нумерованные 1. 2.. Маркер + в обычном списке не используй (в чеклисте + — отдельный синтаксис).

Цитата: >, >>, >>> в начале строки.

Разделитель: строка из минимум трёх дефисов ---.

Изображение

Канон:

!Подпись под картинкой

Важное

Краткая форма (одна строка):

!!! Текст замечания.

Блочная форма:

:::important
Текст замечания.
:::/important

С вариантом:

:::important rule
Не храните пароли в чате.
:::/important
ВариантКогдаЧто рисует система
*(без)*ЗамечаниеИконка info, серый фон
ideaСоветЛампочка, жёлтый фон
problemОшибкаТреугольник, красный фон
solutionРешениеГалочка, зелёный фон
infoСправкаInfo, синий фон
ruleНорма / принципПараграф §, акцентный фон

Иконку система добавляет сама. Не вставляй эмодзи и символы иконок в текст блока.

Не используй important как обёртку всего материала. Каркас урока — обычный Markdown (# / ## / абзацы); important — точечный акцент внутри.

Медиа: единый формат тела

БлокСтрока 1 телаСтрока 2+Пример открытия
audioURLподпись (необязательно):::audio
videoURLподпись (необязательно):::video или :::video wide или :::video 960x540
fileURLимя файла (обязательно для подписи):::file
web_archiveURLподпись (необязательно):::web_archive / wide / WxH
ai_widgetURLподпись (необязательно):::ai_widget / wide / WxH
embedURL === WxH одной строкой:::embed или :::embed wide
picfontURLтекст рядом:::picfont или :::picfont right
picfont chatпосле === Имяголый URL аватара, затем текст реплики:::picfont chat
:::audio
https://cdn.longread.agency/demo/audio-sample.mp3
:::/audio
:::video
https://cdn.longread.agency/demo/video-sample.mp4
:::/video
:::file
https://cdn.longread.agency/demo/file-demo.pdf
Раздаточный материал
:::/file
:::web_archive
https://cdn.longread.agency/demo/archive-demo/index.html
:::/web_archive
:::ai_widget
https://cdn.longread.agency/demo/widget-demo.html
:::/ai_widget

Имя блока — ai_widget (не ai-widget). Голый URL HTML в абзаце или «ссылка в чат» не заменяют блок.

ИИ-HTML через MCP: create-widgetsave-widget-draftbake-widgetinsert-ai-widget-block (fence = id) → poll archives/ по маркеру. Не сдавай по draft. Без вставки в материал задача не сдана. Правила — 05-html-widget-rules.md. Внешние картинки без расширения файла на CDN блокируются при bake.

:::embed
https://longread.media === 640x360
:::/embed

По умолчанию для embed без размеров в теле — 640×360.

:::picfont
https://cdn.longread.agency/demo/picfont-icon.webp
Короткий факт рядом с картинкой.
:::/picfont
:::picfont chat
=== Ассистент
https://cdn.longread.agency/demo/avatar-bot.webp
Ответ ассистента.

=== Пользователь
https://cdn.longread.agency/demo/avatar-user.webp
Вопрос пользователя.
:::/picfont

В :::picfont chat у каждой реплики:

1. === Имя (имя собеседника) 2. Следующая строка — только голый HTTPS-URL аватара (без !, без […], без подписи) 3. Далее текст сообщения

Нельзя (ломает чат — аватар становится огромной картинкой в пузыре):

=== Новичок
![Новичок](https://cdn.longread.agency/images/….webp)
Текст реплики.

Нужно:

=== Новичок
https://cdn.longread.agency/images/….webp
Текст реплики.

При замене локальных путей / CDN-URL внутри :::picfont chat меняй только строку URL. Не превращай её в markdown-картинку ![…](url).

Обычный :::picfont (не chat): тоже голый URL на первой строке тела, текст — со второй. Не ![…](url).

Слайдер

:::slider
=== Кадр 1
![Кадр 1](https://cdn.longread.agency/demo/slider-1.webp)
Текст первого кадра.

=== Кадр 2
![Кадр 2](https://cdn.longread.agency/demo/slider-2.webp)
Текст второго кадра.
:::/slider
:::slider duo
=== Было
![До правки](https://cdn.longread.agency/demo/slider-before.webp)
Интерфейс до изменений.

=== Стало
![После правки](https://cdn.longread.agency/demo/slider-after.webp)
Новый интерфейс.
:::/slider

:::slider wide и :::slider duo wide допустимы.

Интерактивы

Аккордеон

:::acc
=== Заголовок пункта
Содержимое пункта.

=== Второй заголовок
Содержимое второго пункта.
:::/acc

Вкладки

:::tab
=== Вкладка 1
Содержимое первой вкладки.

=== Вкладка 2
Содержимое второй вкладки.
:::/tab

Карточки flip

:::flip
=== Термин
Определение на обороте.

=== Второй термин
Второй ответ.
:::/flip

Допустимо однострочно: лицо === оборот.

Хотспот

:::hotspot
![Схема](https://cdn.longread.agency/demo/hotspot-demo.webp)
=== 30% 40% Зона A
Пояснение зоны A.

=== 1 70% 55% Зона B
Маркер с цифрой 1.
:::/hotspot

Кнопка

Кнопки — только внешние ссылки. Не используй их как навигацию по материалу или проекту: у экспорта проекта есть встроенный плеер.

:::button
Подробнее на Longread.Media === https://longread.media
:::/button
:::button group
=== Longread.Media === https://longread.media
=== Ecourse.Studio === https://ecourse.studio
:::/button

Шаги

:::steps
=== Откройте файл
Дважды щёлкните по ярлыку.
=== Сохраните файл
Ctrl+S.
:::/steps

:::steps — столбец со стрелками ↓. :::steps grid — сетка без стрелок.

Чеклист

Маркер в начале строки задаёт тон пункта:

МаркерВидКогда
+Зелёная галочка ✓Плюс, возможность, «как надо», факт-преимущество
-Красный крест ✗Минус, ошибка, «как не надо»

Список возможностей / фич / преимуществ — только +. Не ставь - «по привычке списка»: - в чеклисте = негатив.

:::checklist
+ Правильный пункт
- Неправильный пункт
Пояснение без маркера
:::/checklist
:::checklist duo
=== + Правильно
Верный подход.
=== - Неправильно
Ошибочный подход.
:::/checklist

Сравнение

Ровно две секции ===:

:::compare
=== Плохо
Неструктурированный текст.
=== Хорошо
Текст с заголовками.
:::/compare

Тест

Открытие — только строка :::test. Режим — следующая строка тела: [test] / [verification] или [think] / [narrative].

НельзяНадо
:::test[test]:::test затем строка [test]
:::test testто же
режим на одной строке с :::режим внутри тела

Режим [test] / [verification]: нужен минимум один + и один -.

:::test
[test]
===
Вопрос?
+ Правильный вариант
- Неправильный вариант
ye: Верно!
no: Подумайте ещё.
:::/test

Режим [think] / [narrative]: вариантов с + нет.

:::test
[think]
===
Что вы думаете?
- Вариант 1
- Вариант 2
- Вариант 3
ok: Спасибо за ответ!
:::/test

Строки ye:, no:, ok: необязательны: если их нет, система подставит дефолтный текст. Пиши их явно, когда нужен свой текст обратной связи.

Заглушка

ОткрытиеВид заглушки
:::tmpКарточка 2:1, подпись «Заглушка»
:::tmp videoПодпись «Видео»
:::tmp simПодпись «Симулятор»
:::tmp dialogПодпись «Диалог»
:::tmp hotspotПодпись «Маркеры»
:::tmp hintТекстовая плашка без картинки
:::tmp
Скоро здесь будет схема
:::/tmp

Колонки

:::columns
70-30
===
Первая колонка.
===
Вторая колонка.
:::/columns
НельзяНадо
50/5050-50 или 1:1
=== Заголовок как разделитель=== пустой; заголовок — ###

Служебное

Строка <<текст>> — метка задачи. Рендерится как текст в угловых кавычках.

CDN-заполнители

БлокURL
Изображениеhttps://cdn.longread.agency/demo/image-demo.webp
Хотспотhttps://cdn.longread.agency/demo/hotspot-demo.webp
Аудиоhttps://cdn.longread.agency/demo/audio-sample.mp3
Видеоhttps://cdn.longread.agency/demo/video-sample.mp4
Файлhttps://cdn.longread.agency/demo/file-demo.pdf
Веб-архивhttps://cdn.longread.agency/demo/archive-demo/index.html
ИИ-HTMLhttps://cdn.longread.agency/demo/widget-demo.html
Picfonthttps://cdn.longread.agency/demo/picfont-icon.webp
Аватар ботhttps://cdn.longread.agency/demo/avatar-bot.webp
Аватар пользовательhttps://cdn.longread.agency/demo/avatar-user.webp
Слайдер…/slider-1.webp, slider-2.webp, slider-3.webp
Слайдер duo…/slider-before.webp, …/slider-after.webp
Обложка проектаhttps://cdn.longread.agency/demo/project-cover.webp

Методика: какой блок выбрать

Используйте эту таблицу для выбора блока под задачу.

Быстрый выбор

ЗадачаБлок
FAQ, глоссарий, однородные пункты «заголовок → текст»:::acc
Равнозначные панели, сценарии:::tab
Термин ↔ определение, самопроверка:::flip
Проверка знаний:::test
Процесс с порядком, инструкция:::steps
Критерии, плюсы/минусы, «как надо»:::checklist
Контраст «плохо ↔ хорошо»:::compare
Важная оговорка, идея, правило!!! или :::important
Кликабельные точки на схеме:::hotspot
Две–три колонки вёрстки:::columns

Когда что использовать

Аккордеон (:::acc)

Используйте для однородных пунктов: заголовок открывает пояснение по клику.

Подходит для:

Не путайте с :::steps. Аккордеон не нумерует пункты.

Вкладки (:::tab)

Используйте для равнозначных панелей с короткими ярлыками.

Подходит для:

Не используйте вкладки для контраста «плохо/хорошо». Для этого — :::compare или :::checklist duo.

Карточки (:::flip)

Используйте для самопроверки: вспомнил → перевернул → проверил.

Лицо — тезис. Оборот — развёрнутый ответ.

Глоссарий из 10+ терминов лучше делать аккордеоном.

Тест (:::test)

Не ставьте [test] с искусственным + там, где подходит [think].

Шаги (:::steps)

Используйте для процесса с порядком: инструкция, алгоритм, «сначала → потом».

Не используйте :::steps для норм без последовательности. Для этого — :::important rule или чеклист.

Чеклист (:::checklist)

Используйте для критериев, требований, списка «что проверить» или списка возможностей.

Отличие от :::compare: чеклист — набор пунктов с ✓/✗. Сравнение — ровно 2 текстовые колонки.

Тест (:::test)

Сравнение (:::compare)

Используйте для контраста подходов. Ровно 2 колонки с заголовками.

Важное (:::important)

Хотспот (:::hotspot)

Используйте для схемы или фото с кликабельными точками.

Требуется реальная картинка ![…](url). Без маркеров блок ведёт себя как обычное изображение.

Колонки (:::columns)

Используйте для параллельной вёрстки: текст + врезка, две равные колонки.

Пишите :::columns:::/columns.

Соотношение: пресет (2:1, 1:1:1) или проценты через дефис (70-30). Не 50/50. Разделитель — пустой ===. Заголовки внутри — ###.

Не заменяйте колонками :::compare или :::checklist duo. В колонках нет семантики «плохо/хорошо».

Не путать

ХотелиНе ставьтеСтавьте
Последовательность шагов:::acc с «Шаг 1» в заголовках:::steps
Норма / принцип:::steps:::important rule
Контраст «плохо / хорошо»Две вкладки:::compare
Много пунктов ✓/✗:::compare:::checklist или :::checklist duo
Глоссарий 10+ терминовПачка :::flip:::acc
Опрос без верного ответа[test] с искусственным +[think]
Две колонки50/50 в первой строке50-50 или 1:1
Заголовок в колонке=== Заголовокпустой === + ### в теле
Свой кликабельный UI (не штатный Q&A):::test / костыль из compare:::ai_widget (после BRIEF, см. 05)

Anti-patterns: wrong → right

Шаги как аккордеон — нельзя

Так нельзя:

:::acc
=== Шаг 1
Откройте файл.
=== Шаг 2
Сохраните файл.
:::/acc

Так надо:

:::steps
=== Откройте файл
Дважды щёлкните по ярлыку.
=== Сохраните файл
Ctrl+S или меню «Файл».
:::/steps

Норма как steps — нельзя

Так нельзя:

:::steps
=== Правило 1
Не храните пароли в чате.
:::/steps

Так надо:

:::important rule
Не храните пароли в чате.
:::/important

Плохо/хорошо вкладками — нельзя

Так нельзя:

:::tab
=== Плохо
Длинный абзац без структуры.
=== Хорошо
Список и заголовки.
:::/tab

Так надо:

:::compare
=== Плохо
Длинный абзац без структуры.
=== Хорошо
Список и заголовки.
:::/compare

Колонки со слэшем — нельзя

Так нельзя:

:::columns
50/50
===
Левая.
===
Правая.
:::/columns

Так надо:

:::columns
50-50
===
### Левая
Текст.
===
### Правая
Текст.
:::/columns

=== Заголовок в колонках — нельзя

Так нельзя (в превью торчит сырой ===):

:::columns
1:1
=== Левая
Текст.
=== Правая
Текст.
:::/columns

Так надо — пустой ===, заголовок через ### (см. пример выше).

Опрос с фейковым «верным» — нельзя

Так нельзя:

:::test
[test]
===
Как вам курс?
+ Отлично
- Нормально
ye: Верно!
no: Неверно.
:::/test

Так надо:

:::test
[think]
===
Как вам курс?
- Отлично
- Нормально
- Сложно
ok: Спасибо за ответ!
:::/test

Режим теста на строке открытия — нельзя

Так нельзя:

:::test[test]
===
Вопрос?
+ Да
- Нет
:::/test

Так надо:

:::test
[test]
===
Вопрос?
+ Да
- Нет
:::/test

Фичи чеклистом с минусами — нельзя

Так нельзя (все пункты красные ✗):

:::checklist
- Поддерживает мультимодальные запросы
- Использует Query Fan-Out
:::/checklist

Так надо (зелёные ✓):

:::checklist
+ Поддерживает мультимодальные запросы
+ Использует Query Fan-Out
:::/checklist

Весь урок в important — нельзя

Так нельзя: открыть :::important idea в начале файла и закрыть :::/important в конце, а внутри — #, ##, steps, compare.

Так надо: обычный Markdown-каркас; :::important только на короткий акцент (несколько предложений).

Блок в одну строку — нельзя

Так нельзя:

:::steps === Шаг 1 Текст. === Шаг 2 Текст. :::/steps

Так надо:

:::steps
=== Шаг 1
Текст.
=== Шаг 2
Текст.
:::/steps

То же для :::compare и :::checklist duo.

Размещение и объём

Не выдумывайте несуществующих типов блоков. Используйте только идентификаторы из допустимого списка.

MCP-инструменты Демонстратора

Карта инструментов (имя → зачем). Полные схемы параметров не дублируются здесь.

Параметры инструментов

После подключения MCP клиент вызывает tools/list. Сервер отдаёт канонические inputSchema (required, типы, enums, лимиты).

Подключение к MCP

Подпишись на remote MCP-шлюз на сервере Демонстратора. Клонировать репозиторий и запускать node локально не нужно.

Токен уже лежит в конфиге MCP-клиента в IDE (mcp.jsonheaders.Authorization, значение после Bearer ). В Личном кабинете он генерируется один раз при первичной настройке — не запрашивай его заново и не ищи в переменных окружения.

{
  "mcpServers": {
    "demonstrator-mcp": {
      "url": "https://app.longread.agency/mcp/demonstrator/mcp",
      "headers": {
        "Authorization": "Bearer <токен_из_mcp.json>"
      }
    }
  }
}

Эндпоинты шлюза:

МетодURLНазначение
GEThttps://app.longread.agency/mcp/demonstrator/healthПроверка живости
GEThttps://app.longread.agency/mcp/demonstrator/sseSSE (keepalive; Cursor сюда url не ставь)
POSThttps://app.longread.agency/mcp/demonstrator/mcpJSON-RPC / streamable HTTP — это URL для IDE

Happy path: материал с нуля

1. list-projectsproject_id (или create-project). 2. create-materialtitle + project_id. 3. Markdown по 01-syntax.md / 02-methodology.md. 4. Запись тела — только через Yjs collab-мост, не прямым PATCH в БД:

или save-material-content (тот же POST /api/collab/materials/:id/patch);

5. Проверить verify.ok и verify.integrity.ok / get-material. source: yjs_room или db_fallback. Не сдавай по одному verify.ok.

Запрещено писать markdown через raw PATCH /api/materials с полем content (tool update-material-content удалён): открытый редактор перезапишет старым Yjs-документом.

Полный маршрут и сценарий без MCP — в README.md.

Каталог (обзор)

Ниже — имя и краткое действие. Схемы полей — в tools/list.

Проекты и пространства

ИнструментДействие
list-projectsПолучить список проектов
create-projectСоздать проект
update-projectОбновить проект
list-spacesПолучить список пространств
get-spaceПолучить пространство
update-spaceПереименовать пространство
list-space-membersСписок участников (только чтение)

Создание пространства, приглашения, смена состава/ролей, permanent delete-project — только через UI, не через MCP.

Структура

ИнструментДействие
list-sectionsСписок разделов (с sort_order, среди разделов)
create-sectionСоздать раздел
update-sectionОбновить раздел (в т.ч. одиночный sort_order)
reorder-sectionsПорядок только среди разделов (sort_order)
delete-sectionУдалить раздел (soft delete)
list-materialsСписок материалов (с sort_order)
search-materialsПолнотекстовый поиск по материалам
create-materialСоздать материал
duplicate-materialКлонировать материал (суффикс «(копия)»)
rename-materialПереименовать материал (только title, контент не меняется)
update-material-placementПереместить материал / один sort_order
reorder-materialsПорядок уроков внутри раздела (или среди корневых)
get-project-sequenceСмешанный порядок карточек проекта: раздел ↔ корневой прототип
patch-project-sequenceЗадать смешанный порядок (как DnD в ЛК)
delete-materialУдалить материал (soft delete)

Порядок: три разных оси (не путать)

Что меняешьToolГде видно
Раздел ↔ корневой прототип вперемешкуget-project-sequence / patch-project-sequenceсетка карточек проекта в ЛК
Порядок разделов между собойreorder-sections (sort_order)среди разделов
Порядок уроков внутри разделаreorder-materials + section_idвнутри раздела

Можно чередовать: раздел → прототип → раздел → прототип. Это штатный project_sequence, не запрет продукта.

Стартовый/пустой sequence (миграция + UI-fallback) часто выглядит как «сначала все разделы, потом корневые» — дефолт, не закон. Не сочиняй «разделы всегда сверху» / «без разделов только плоский список».

Дерево слева sequence почти не отражает: смешанный список — в сетке карточек.

patch-project-sequence: items: [{ "item_type": "section"|"material", "item_id": N }, …] сверху вниз. В items только корневые материалы (без section_id). Перед вызовом — подтверждение пользователя (см. СТОП в README.md).

reorder-sections / reorder-materials не умеют смешивать типы — для этого только sequence.

Медиа

ИнструментДействие
list-resourcesСписок ресурсов (фильтр: projectid, materialid, q)
presign-media-uploadInit upload audio/video/file/web_archive → resource_id + upload_url (PUT с машины клиента)
complete-media-uploadComplete после PUT → id, status, full_url (CDN), markdown_snippet
upload-media-from-fsЗагрузка по file_url (HTTPS) или file_path уже на сервере MCP — не Windows-path клиента
get-resource-statusСтатус обработки медиа (poll до ready / failed)
insert-media-blockВставить медиа-блок; display_name / name → имя в медиатеке
rename-resourceПереименовать ресурс

Permanent удаление ресурса — только UI (delete-resource в MCP нет).

Upload и insert: осмысленный filename / display_name. «Без названия» в медиатеке — брак; сразу rename-resource.

Kind и CDN URL — не путать

В markdown / обложку / логотип ставь только full_url (или url) из ответа upload / list-resources / get-resource-status. Не собирай CDN-путь руками и не «чини» подстроки в URL.

НазначениеkindКак залитьТипичный full_url
Картинка для показа (логотип, обложка, иллюстрация, ![…](…))imagecurl multipart (ниже) или upload-media-from-fs с kind: imagehttps://cdn.longread.agency/images/{hash}.webp (растр) или …/{hash}.svg (SVG)
Скачиваемый файл (PDF, ZIP, DOCX…)filepresign → PUT → complete…/files/originals/{id}.{ext} — так и должно быть
Аудио / видео после readyaudio / videopresign → ……/audio/processed/… или …/video/processed/…
ИИ-HTMLweb_archive / bakeсм. виджеты…/archives/{id}/…

Частая ошибка: логотип/обложку залить как file (получится files/originals/…), потом «исправить» на выдуманный files/processed/…. Пути files/processed/ нет. Для показа — перезалей как image и возьми новый full_url с /images/…webp.

Картинка-image: jpeg / png / webp / gif / svg. SVG принимается как image без растеризации — сервер санитизирует (вырезает скрипты/обработчики) и хранит как …/images/{hash}.svg. Растр (jpeg/png/webp/gif) пережимается в …/images/{hash}.webp. SVG как file — только для скачивания, не для показа.

Как загрузить локальный файл → Object Storage → CDN

MCP не читает файлы с диска агента (C:\…). Не вызывай upload-media-from-fs с Windows-path. insert-media-block файл не загружает. Сначала upload, потом insert.

Запрещено

Дерево решений: как загрузить файл

Файл — картинка для показа? (PNG/JPEG/WebP)
  ├─ Файл локально на твоём диске (C:\...)
  │   → Shell curl multipart (↓ см. «Картинка — curl»).
  │   → НЕ вызывай recipe-upload-and-insert / upload-media-from-fs с file_path.
  │   → НЕ вызывай presign-media-upload (пресайн не для image).
  │
  ├─ Файл уже на публичном HTTPS
  │   → recipe-upload-and-insert с file_url + kind: image
  │
  └─ Файл на сервере MCP (не твоя машина)
      → upload-media-from-fs с file_path

Файл — аудио / видео / file / web_archive?
  ├─ Файл локально на твоём диске
  │   → presign-media-upload → Shell curl PUT → complete-media-upload
  │   → ИЛИ recipe-upload-and-insert если не нужно вручную PUT
  │
  ├─ Файл на публичном HTTPS
  │   → recipe-upload-and-insert с file_url
  │
  └─ Файл на сервере MCP
      → upload-media-from-fs с file_path

#### Выбор пути

Что на диске агентаКак
Картинка для показа (image)Shell curl multipart (ниже)
audio / video / file / web_archiveMCP presign-media-upload → Shell PUT → MCP complete-media-upload
Уже HTTPS URLMCP upload-media-from-fs с file_url + material_id/project_id (+ верный kind)

После upload вызови MCP insert-media-block или вставь ![…](full_url) в content. Processing-медиа: get-resource-status до ready / failed. На каждом upload передай material_id или project_id.

ASCII-only на диске. Путь и имя файла для upload — только латиница/цифры (C:\work\media\fig-01.png). Кириллица в папках или имени файла ломает curl у агентов. Русское имя в медиатеке — после upload: rename-resource или display_name / name в insert-media-block.

#### Картинка — curl

Токен — тот же Bearer, что стоит в конфиге MCP-клиента в IDE (сервер demonstrator-mcp / user-demonstrator-mcp). Не ищи токен в переменных окружения шелла — там может лежать старый/отозванный токен после ротации. Если в конфиге IDE токена нет — спроси у пользователя. Prod API: https://app.longread.agency.

Если POST /api/resources отвечает 401 — токен не тот или ротирован: стоп и спроси у пользователя актуальный токен. Не перебирай варианты.

curl.exe -sS -X POST "https://app.longread.agency/api/resources" `
  -H "Authorization: Bearer ТОКЕН_ИЗ_КОНФИГА_MCP" `
  -H "Accept: application/json" `
  -F "file=@C:\work\media\fig-01.png;filename=fig-01.png;type=image/png" `
  -F "material_id=123" `
  -F "original_name=fig-01.png"

1. Из JSON возьми id и full_url (или url) — должен быть путь /images/…webp. 2. Вызови insert-media-block (kind: image) или вставь ![подпись](full_url). 3. Задай читаемое имя. Иначе вызови rename-resource.

Для image нет presign-media-upload. Нет MCP-tool, который читает локальный png. Если upload-local-media.ps1 уже лежит в текущем workspace — можно вызвать его вместо curl. Иначе делай curl. Не сканируй диск.

#### audio / video / file / web_archive

1. MCP presign-media-upload (kind, filename, size, material_id или project_id). 2. Shell: curl.exe -sS -X PUT "<upload_url>" -H "Content-Type: …" --data-binary "@C:\path\to\file". 3. MCP complete-media-upload (тот же material_id / project_id). 4. MCP insert-media-block.

kind: file — только для скачиваемых вложений. Не для логотипа/обложки/блока картинки.

#### Уже публичный HTTPS

MCP upload-media-from-fs: только file_url (https://…). Не C:\…. Для показа картинки — kind: image.

Медиатека проекта

Медиатека = resource_links на project_id / material_id.

Что не делать

ИИ-HTML (:::ai_widget)

ИнструментДействие
create-widgetСоздать черновик ИИ-HTML. Обязательно name (или caption / original_name) — имя в медиатеке
get-widgetПолучить ИИ-HTML; у linear_dialog — ещё archetype + scenario
save-widget-draftСохранить HTML в черновик (свободная вёрстка). Не для правок линейного диалога
assemble-linear-dialogСобрать или обновить линейный диалог из полного scenario JSON (без Studio AI). Фича ai_widget_templates (тестеры)
bake-widgetСобрать ИИ-HTML в готовый архив
fork-widgetФоркнуть ИИ-HTML в другой материал
insert-ai-widget-blockВставить :::ai_widget в материал

Имя в медиатеке

Ресурс без нормального original_name в UI выглядит как «Без названия» / бессмысленный дефолт. Это брак.

Не готово: виджет/файл в медиатеке без читаемого имени.

Путь создания ИИ-HTML (только IDE + MCP)

локальный .html + маркер + отладка → вопрос «bake на CDN?» → «да» →
create-widget → save-widget-draft → bake-widget (ready) →
insert-ai-widget-block (fence = id, не draft) →
poll archives/ до совпадения маркера → get-material

1. Запиши и отладь HTML в workspace (widgets/<slug>.html). Добавь уникальный маркер. 2. Спроси пользователя про bake/insert. Жди прямого «да». Не вызывай bake-widget на каждой правке. 3. create-widgetsave-widget-draft (HTML с диска) → bake-widget. 4. insert-ai-widget-block — в блок только CDN entry_url после bake (ready). Draft URL и bare id в материал запрещены. 5. Poll: get-resource-status / archive-info → HTML с archives/ (entry_url, учти ?v=). Маркер должен совпасть с локальным файлом. get-widget и draft — не критерий сдачи. 6. get-material — fence на месте. Путь к локальному файлу — пользователю.

В create-widget всегда передай name (имя в медиатеке). Без имени — не готово; при ошибке — rename-resource.

Шаблон: линейный диалог (assemble-linear-dialog)

Когда нужен диалоговый плеер, не пиши HTML с нуля — собери сценарий JSON и вызови assemble-linear-dialog. Shell канонический; агент заполняет данные (пул портретов, локации, реплики с парой слотов).

Предпочтительный HAPPY PATH: recipe-linear-dialog (создаёт виджет + собирает диалог + валидирует схему, ловит portrait_url и пропущенный id).

scenario.json на диске → recipe-linear-dialog (или create-widget → assemble-linear-dialog) →
вопрос «bake на CDN?» → «да» → bake-widget → insert-ai-widget-block (CDN URL)

#### Схема сценария (и частые ошибки)

Эталонный полный пример: docs/templates/linear-dialog/scenario.example.json. Основные поля:

ПолеПравильноЧастая ошибка
archetype"linear_dialog" внутри scenarioкладут в корень manifest'а, не в scenario
Персонаж — портретimageUrl + portrait: "auto"portrait_url (несуществующий ключ)
Персонаж — IDid: "p1"нет поля id
Шаг — IDid: "s1" обязательнопропускают id
Шаг — локацияlocationId обязательнонет locationId → 422
Шаг — портретыleftPortraitId, rightPortraitId (два слота)один слот или нет пары
Шаг — кто говоритspeakerSide: "left" или "right", плюс speaker = id персонажанет speaker или неясно
Шаг — следующийnext: "s2" или null у последнеговсе null — плеер остановится после первого
locations[]обязателен (минимум одна, можно с пустым imageUrl)не передают — 422 «опционально» в старых текстах ошибочно
locations[].idid: "loc1"нет id

1. Сохрани scenario локально (widgets/<slug>-scenario.json). Начни с docs/templates/linear-dialog/scenario.example.json — не угадывай поля. 2. На сцене всегда два слота картинок; characters[] — N вариаций (ракурсы/эмоции). На каждой реплике задай пару портретов и кто говорит (speakerSide: left|right). 3. Демо-медиа CDN: avatar-bot.webp, avatar-user.webp, slider-1.webp на cdn.longread.agency/demo/. 4. create-widget с nameassemble-linear-dialog (widget_id + scenario). Не нужен save-widget-draft. 5. Bake/insert — только после прямого «да» (как у свободного HTML). Insert пишет CDN entry_url, не id. 6. При 403 ai_widget_templates_forbidden — шаблоны только у тестеров; не обходи через свободный HTML без согласования.

Правка существующего линейного диалога

Тот же assemble-linear-dialogсоздаёт и обновляет. Отдельного «refine» в MCP нет (Studio AI через MCP не вызываем).

get-widget → взять scenario → править JSON (локально или в ответе) →
assemble-linear-dialog (полный scenario) → при необходимости снова bake после «да»

1. get-widget (widget_id) → поля archetype: linear_dialog и scenario. 2. Если scenario пуст — вытащи из HTML #vn-scenario или восстанови из локального widgets/<slug>-scenario.json. 3. Внеси правки в полный scenario (реплики, портреты пула, leftPortraitId/rightPortraitId/speakerSide, локации, порядок steps / next). Не отправляй diff / SEARCH-REPLACE по shell. 4. Обнови локальный widgets/<slug>-scenario.json. 5. assemble-linear-dialog с тем же widget_id и полным scenario — пересоберёт draft. 6. Запрещено для диалога: save-widget-draft с ручным HTML плеера; правка CSS/JS shell; вызов /api/ai/widgets/*/refine*. 7. Повторный bake-widget / insert — только после прямого «да» пользователя (как при создании).

Чат-боты без MCP этот путь не используют. Они отдают HTML/markdown кодом и bake не делают.

Не готово (IDE): только bare id в fence; bake без «да»; bake без локального файла; bake без insert; сдача по draft при отстающем archive; fence с draft_url; ресурс в медиатеке без читаемого имени.

Серверный контракт — 05-html-widget-rules.md.

Корзина

ИнструментДействие
list-project-trashСписок удалённого
restore-project-trashВосстановить (items: [{type, id}] или all: true)

Share-ссылки

Создание и обновление — да. Снятие / unpublish ссылок через MCP нельзя (только UI).

ИнструментДействие
get-material-share-linksСсылки материала
create-material-share-linkСоздать ссылку материала
update-material-share-linkОбновить уже созданную ссылку материала (auto_update)
get-project-share-linksСсылки проекта
create-project-share-linkСоздать или обновить (refresh_only: true — без ротации токенов)
get-section-share-linksСсылки раздела
create-section-share-linkСоздать или обновить (refresh_only)
republish-comments-snapshotОбновить HTML-snapshot для публичной ссылки с правками. Не пишет тело урока (content)

Ответ create/get содержит url — полный канонический URL.

Типы ссылок: preview (просмотр), preview_comments (с комментариями).

Comment/share ≠ editor body. republish-comments-snapshot, share-links с preview_comments и get-material-preview-with-comments — про публичный review/HTML. Текст урока в редакторе правят только save-material-content / apply-material-point-patch. В ответах материала нет внутренних publish-кэшей — только content.

Чтение и экспорт

ИнструментДействие
get-materialid, title, content, theme, version, placement (whitelist)
get-material-export-dataMarkdown + тема материала
get-section-export-dataРаздел + все материалы с контентом
get-project-export-dataДерево проекта для HTML-ZIP
get-material-preview-with-commentsRead-only HTML snapshot+overlay; не источник для правки markdown

Предложения правок

Сейчас вне MCP (недоделано; вернём позже). Пиши тело сразу через collab (save-material-content / apply-material-point-patch).

Запись контента

Источник правды при открытом редакторе — Yjs collab, не строка в БД. Все правки тела материала — через collab-мост.

ИнструментДействие
apply-material-point-patchКанон. replace_all / range_patch / find_replace_once/api/collab/materials/:id/patch
save-material-contentПолная замена тела тем же collab-мостом (replace_all) + опционально title/theme. По умолчанию verify_readback: true
apply-material-themeТема (palette / contentStyle / customOverlayCss)

Коридор при открытом редакторе

РежимВкладка закрыта (db_fallback)Вкладка открыта (живая комната)
replace_all / save-material-contentокок → yjs_room (live в Коде, тост MCP). 409 возможна только при редкой гонке reconnect — повтори
range_patch / find_replace_onceок (одна-две мелкие правки)409 point_patch_blocked_editor_open

Серия точечных патчей поверх живого Yjs даёт дубли #/##, обрыв+повтор куска, голые === / :::/…. При открытой вкладке: range_patch и find_replace_once заблокированы (409), replace_all / save-material-content пишут в Yjs, контент приходит в Код как remote change (тост «обновлён с сервера (MCP)»). Не «добивай» пятью find_replace_once после 409.

Future (чат-ассистент по открытому материалу): настоящий agent-WS peer с мелкими Y.Text ops «как юзер» — отдельно от массовой авторской переписки урока; массовые правки всё равно через replace_all + integrity.

Режимы apply-material-point-patch

РежимКогда использовать
replace_allЗамена всего контента (основной путь «записать материал»; единственный при открытом редакторе)
range_patchПравка по смещению — только при закрытой вкладке
find_replace_onceЗамена по тексту — только при закрытой вкладке; не сериями на большом diff

Все write-операции используют expected_version (tool подставит сам, если не передан).

Ответ содержит source:

replace_all / save-material-content при открытой вкладке: контент пишется в Yjs → приходит в Код как remote change (тост «обновлён с сервера (MCP)»). 409 (editor_open_room_not_ready / collab_room_push_failed) возможна только при редкой гонке reconnect — повтори запрос.

Точечный патч при открытой вкладке → 409 point_patch_blocked_editor_open. Используй один replace_all целым телом.

После записи смотри в ответе:

1. verify.ok — байтовое равенство отправленного и прочитанного (не «материал логически целый»). 2. verify.integrity.ok (и/или integrity в ответе collab patch) — структура: нет дублей #/##, нет голых === / orphan :::/…, нет незакрытых блоков, нет повторённого куска. 3. source и хвост get-material глазами при сомнении.

Не сдавай, если verify.ok true, а integrity.ok false. Надёжнее один финальный replace_all / save-material-content целым файлом, чем много мелких find_replace_once.

Не пишите тело через raw PATCH «в базу». Вторичных «workspace»/publish body-полей в ответах нет — править нечего, кроме content через collab.

Расхождение с readback: сначала факты, потом диагноз

Не списывай расхождение «отправил — прочитал» на «потерялось при ре-синхронизации Yjs», пока не назвал факты последнего write-вызова.

1. Был ли write-вызов. Если save-material-content / apply-material-point-patch на этот material_id не вызывался, правка в бэкенд не уходила — Yjs тут ни при чём. Ищи, что вообще не записали (или записали не в тот материал). 2. source ответа (обязательно назвать):

3. new_version / updated_at. Если версия материала не сдвинулась на момент записи (например, запись «~12:30», а updated_at «~09:29») — запись не дошла до БД. Сверь material_id и был ли ответ ok: true.

Антипаттерн: «потерялось при ре-синхронизации Yjs» при source: db_fallback или при отсутствии write-вызова в истории. Это блокер-регрессия, не «известная проблема».

Темы

ИнструментДействие
list-themesСписок пресетов: minimalism, draft, journal, neon, airy, modern
apply-material-themeПрименить тему. Palette / contentStyle / customOverlayCss; также bgImageResourceId + bgImageMode (pattern \frame) для фона-картинки из медиатеки
inherit-project-themesСбросить темы разделов/уроков проекта на наследование от темы проекта

apply-material-theme принимает произвольный theme object (merge через MERGE_KEYS). Фон-картинка:

Не подставляй картинку фона через customOverlayCss (url(...)) — используй поля темы. URL на shared/export резолвит платформа (bg_image_url).

Правила CSS для customOverlayCss — см. раздел «Правила CSS-генерации».

Правила CSS тем (customOverlayCss)

CSS для apply-material-themecustomOverlayCss. Это ограничения, не гайд по дизайну.

Фон страницы (shared / export)

В shared колонка текста — body (max-width 720px), а холст страницыhtml на весь viewport. Класс .longread-theme-custom вешается и на html, и на body; у колонки background: transparent !important.

Канон shared красит холст так:

html:has(body.shared-view) {
  background-color: var(--longread-bg);
  background-image: var(--longread-bg-image, none);
  background-size: var(--longread-bg-image-size, 960px auto);
}

Специфичность html:has(body.shared-view) выше, чем у .longread-theme-custom. Один только background: …gradient… на классе не перекрывает канон: на html остаётся solid --longread-bg и background-image: none.

ЗадачаКак
Плоский фон--longread-bgтолько solid #hex (нижний слой и для color-mix).
Картинка из медиатекиПоля темы bgImageResourceId + bgImageMode (pattern \frame). Не пиши url(...) руками в overlay.
Градиент на весь viewport--longread-bg-image + --longread-bg-image-size: 100% 100% на .longread-theme-custom (см. ниже).
Не делатьСелекторы html / body; gradient(...) в --longread-bg; считать задачу решённой после одного background: …gradient… на классе без переменных и без проверки shared.

Градиент на всё окно (shared / export)

1. theme.bg / --longread-bgтолько solid. 2. Визуальный градиент на весь viewport задавай через переменные, которые читает канон холста:

.longread-theme-custom {
  --longread-bg-image:
    radial-gradient(
      ellipse 80% 50% at 70% 0%,
      color-mix(in srgb, var(--longread-accent) 40%, transparent),
      transparent 70%
    ),
    linear-gradient(
      180deg,
      color-mix(in srgb, var(--longread-surface) 85%, var(--longread-accent)) 0%,
      var(--longread-bg) 100%
    );
  /* иначе дефолт 960px auto — плитка, не «на всё окно» */
  --longread-bg-image-size: 100% 100%;
}

3. Без --longread-bg-image холст shared останется плоским, даже если на .longread-theme-custom есть background: …gradient…. 4. Не ставь селекторы html / body. Не клади gradient(...) в --longread-bg. 5. --longread-bg-image — тот же хук, что у картинки из медиатеки (bgImageResourceId). Свой градиент в переменной перебивает page-image — авторская ответственность. С bgImageMode: frame full-bleed градиент через эту var может вести себя иначе; проверяй shared без frame либо не мешай с frame. 6. Перед сдачей — shared-view (hard refresh): у html computed background-image содержит gradient, background-size — на весь холст (100% 100% или эквивалент), а не пятна только на узкой колонке.

Антипаттерн: background: radial-gradient… на .longread-theme-custom без --longread-bg-image / --longread-bg-image-size и без проверки shared.

Штатный фон-картинка (тема)

Цвета — только переменные

Цвета темы материала задаются через --longread-*. В customOverlayCss для любого цвета (color, background, background-color, border-color, outline-color, box-shadow с цветом, text-shadow с цветом, fill, stroke) пиши только var(--longread-…).

ПеременнаяНазначение
--longread-bgБазовый фон страницы (solid)
--longread-textТекст
--longread-surfaceФон карточек / поверхностей
--longread-surface-textТекст на surface
--longread-headingsЗаголовки
--longread-accentАкцент
--longread-accent-paleБледный акцент
--longread-borderГраницы
--longread-positive«Хорошо»
--longread-negative«Плохо»
--longread-important-bgФон important

Отступы темы (не цвета, но тоже из темы): --longread-content-padding, --longread-block-padding, --longread-block-margin.

Запрещено (цвета)

/* Верно */
.longread-theme-custom h1 { color: var(--longread-headings); }
.longread-theme-custom .block.btn a {
  background: var(--longread-accent);
  color: var(--longread-surface);
}

/* Запрещено */
.longread-theme-custom h1 { color: #1f3249; }
.longread-theme-custom .card { background: white; }

Исключение: прозрачность через color-mix / oklch от var(--longread-*) — ок. Чистый hex — нет. Иной цвет без переменной — только после прямого «да» пользователя.

Self-lint перед apply-material-theme: в CSS нет # / rgb( / rgba( / hsl( для цветов (кроме редкого согласованного исключения).

Селекторный контекст

Не ломай вёрстку

Без прямого «да» пользователя не трогай: display, position, flex, grid, width, height, overflow.

Не стилизуй без «да»: .test-options, radio/checkbox внутри теста, их ::before / ::after / ::marker.

Ограничения:

Безопасный коридор по умолчанию: color, background, border*, border-radius, box-shadow, text-shadow, font-*, letter-spacing, padding/margin (осторожно), transform.

Классы блоков (шпаргалка селекторов)

Стилизуй только нужное. Цвета — снова только var(--longread-*).

БлокСелекторы
Заголовкиh1h6
Важное.block.important, модификаторы .idea / .problem / .solution / .info / .rule
Аккордеон.accordion, .accordion-item, .accordion-head, .accordion-body
Вкладки.tabs-block, .tabs-nav, .tabs-panel
Карточки.flip-front, .flip-back
Тест.test-block, .test-question, .test-feedback
Шаги.steps-item, .steps-num, .steps-title, .steps-body
Чеклист.block.checklist, .checklist-item
Сравнение.compare-col-good, .compare-col-bad
Кнопка.block.btn a
Прочее.block.img img, .block.audio, table/th/td, .block.embed, .comments-block

Вложенные blockquote (три уровня) — не ломай структуру; цвета уровней тоже через --longread-*.

Правила ИИ-HTML (:::ai_widget)

Имя блока — ai_widget (подчёркивание). Не пиши ai-widget, aiwidget или «просто ссылка на HTML».

ИИ-HTML — самодостаточный HTML. Он обязан стоять в материале как блок :::ai_widget:::/ai_widget. Без этого блока в редакторе виджета для пользователя нет. Сверстать, залить на CDN и прислать URL — не выполнение задачи.

ИИ-HTML всегда часть материала. Он берёт CSS-переменные темы (--longread-*) у родительского лонгрида и живёт в его вёрстке. Без лонгрида нет переменных темы, блочных отступов и рендер-контекста. Не делай из ИИ-HTML отдельную страницу.

Перед create / bake

Не изобретай ИИ-виджет «от себя». Сначала кратко: зачем виджет, что делает пользователь, чем не закрывается штатными блоками (:::test, :::tabs, …). Дождись согласия («да» / BRIEF ок). Потом локальный .html и цикл ниже. Bake/insert — только после отдельного прямого «да».

Обязательный финал (только IDE-агент с MCP)

Это правило — для Cursor / Claude Code / VS Code с MCP. Чат-боты без MCP (Gemini, ChatGPT и т.п.) bake не делают. Они отдают HTML/markdown кодом. См. сценарий 2 в README.md.

С MCP работа в материале готова после вставки :::ai_widget. Bake на CDN — не на каждой итерации.

Цикл отладки (канон)

локальный .html (+ маркер) → открыть в браузере / править на диске → …
→ спросить «запечь на CDN и вставить в материал?» → ждать прямого «да»
→ create-widget (если ещё нет) → save-widget-draft → bake-widget
→ insert-ai-widget-block (fence = id) → poll archives/ до маркера

Пока пользователь не сказал «да» на bake — правь только локальный файл. Не вызывай bake-widget «на всякий случай». Не пеки после каждого фикса.

После правок уже вставленного виджета снова правь локальный .html. Отладь локально. Снова спроси перед следующим save-widget-draft + bake-widget. Не bake каждый раз сам.

После bake/insert: poll archives/ (не draft). Затем get-material. В content должен быть :::ai_widget с id.

Локальная копия на ПК (обязательно при MCP)

Интерактив через MCP в Демонстраторе — ок. На диске рабочей машины всё равно нужна HTML-копия. По ней смотришь и правишь без share/CDN-ссылок.

Обязательно:

1. Сохрани полный HTML в workspace (widgets/ или interactives/), например widgets/quiz-onboarding-<widget_id>.html (или slug без id, пока виджета ещё нет). 2. Отладь локально (открой файл в браузере). Это основной цикл. 3. В ответе укажи локальный путь. 4. Bake/insert — только после прямого вопроса и прямого «да» пользователя.

Запрещено: bake на каждой правке; только MCP/CDN без файла на диске; «открой по ссылке» как единственный способ смотреть интерактив.

Запрещено считать задачу сделанной (IDE + MCP)

Чат-бот без MCP

Отдай готовый HTML и/или фрагмент markdown с :::ai_widget:::/ai_widget. Bake, CDN и вопрос «запечь?» к тебе не относятся. Пользователь вставит сам.

Вёрстка и отступы (не липнуть к потолку)

ИИ-HTML живёт внутри материала (iframe / карточка блока). Не делай из него полноэкранный лендинг вплотную к краям.

Обязательно:

Запрещено:

html, body { margin: 0; padding: 0; } /* и сразу hero/img на всю ширину без обёртки с отступом */
.card { margin: 0; } /* карточка вплотную к верху iframe */
.hero { width: 100%; margin: 0; } /* картинка к потолку виджета */

Нужно (идея):

body {
  margin: 0;
  padding: 20px; /* воздух от краёв блока в лонгриде */
  box-sizing: border-box;
}
.card {
  overflow: hidden;
  border-radius: 12px;
  /* картинка может быть edge-to-edge ВНУТРИ card — снаружи уже есть padding body */
}

Виджет без верхнего воздуха — брак вёрстки, переделай до bake / insert.

Русский текст внутри виджета — по 06-russian-typography.md (—, «ёлочки», …).

Скролл внутри виджета

По умолчанию без скролла. Короткие реплики, обратная связь и описание ситуации должны целиком помещаться в кадр. Не ставь overflow: auto|scroll и фиксированную высоту «наугад», если контент можно уложить или разбить на шаги.

Скролл — только явный замысел. Пример: длинный текст, который пользователь сознательно читает или открывает дополнительно.

КонтентСкролл
Реплика персонажаНет
Обратная связь к ответуНет
Короткое описание ситуацииНет
Развёрнутое объяснение последствийДа (если иначе не влезает)
Уточнение деталей по кнопкеДа

Запрещено: весь экран кейса / квиза в одном скролл-боксе «на всякий случай». Скролл ради ленивой вёрстки короткого текста.

Тема и цвета (обязательно)

Внутри :::ai_widget цвета темы приходят как --longread-* на :root (сервер / THEME). Любой цвет в CSS виджета — только var(--longread-…).

ПеременнаяНазначение
--longread-bgФон
--longread-textТекст
--longread-surfaceФон карточек
--longread-surface-textТекст на карточках
--longread-headingsЗаголовки (если есть)
--longread-accentАкцент
--longread-accent-paleБледный акцент
--longread-positiveЗелёный
--longread-negativeКрасный
--longread-important-bgФон important
--longread-borderГраницы
button {
  background: var(--longread-accent);
  color: var(--longread-surface);
}

Запрещено

Self-lint перед сдачей HTML: в <style> нет цветовых литералов вне var(--longread-*) (кроме согласованного исключения).

Готовых CSS-китов нет — классы свои, цвета только из переменных.

<style id="widget-escape"> — CSS внутри переживает переинжекцию темы; цвета там всё равно через --longread-*, не hex.

Запрещено сервером

Разрешено

Ограничение размера

500 КБ. Сервер отклоняет HTML больше этого лимита.

Изображения

Только с CDN проекта. URL должен заканчиваться на одно из: .webp, .png, .jpg, .jpeg, .gif, .svg.

MCP-путь (IDE)

1. Сохрани и отладь HTML локально (widgets/<slug>.html). Это основной цикл. Перед bake поставь в HTML уникальный маркер (комментарий / data-build). 2. Спроси: «запечь на CDN и вставить в материал?» Жди прямого «да». Не bake без ответа. 3. create-widget — получи id черновика (material_id / project_id). Всегда передай name — читаемое имя в медиатеке (не uuid, не пусто). 4. save-widget-draft — HTML с диска + theme + assets. 5. bake-widget — дождись статуса ready (ключ archives/{id}/…). 6. insert-ai-widget-block — в fence только CDN entry_url (после ready). Не bare id и не draft_url. 7. Poll archive: get-resource-status / archive-infoentry_url / archives/…. Маркер в archive HTML должен совпасть с маркером локального файла. Пока не совпало — poll. Не сдавай по get-widget / draft. 8. get-material — проверь fence. Пользователю укажи путь к локальному .html.

Шаблон линейного диалога

Для диалогового плеера предпочитай assemble-linear-dialog вместо свободного HTML:

1. Сценарий JSON на диске (widgets/<slug>-scenario.json) — эталон docs/templates/linear-dialog/scenario.example.json. characters[] = пул портретов; на реплике — leftPortraitId, rightPortraitId, speakerSide. 2. create-widgetassemble-linear-dialog (widget_id + scenario). save-widget-draft не нужен. 3. Bake/insert — только после прямого «да». Insert = CDN URL. 4. Фича ai_widget_templates — по умолчанию тестеры; при 403 не обходи без согласования.

Правка уже собранного диалога (тот же tool):

1. get-widgetscenario (+ локальный *-scenario.json). 2. Правишь JSON (реплики / портреты / пары слотов / локации) — полный объект, не патч HTML. 3. Снова assemble-linear-dialog с тем же widget_id. 4. Не вызывай save-widget-draft и не правь shell плеера руками. 5. Повторный bake — только после «да»; в материал снова CDN entry_url.

Имя в медиатеке обязательно. Если создал без имени — сразу rename-resource. «Без названия» / пустой ярлык — брак сдачи.

СлойВ материал?
Draft / draft_url / bare id / свежий get-widgetнет
archives/{id}/ CDN entry_url при readyда

После bake CDN может кратко отдавать старое поколение на том же URL. Держи poll archive. Не ставь draft в материал. Повторный bake — только после явного «да».

Повторные правки: снова локально → снова вопрос перед bake. Не пеки на каждой итерации.

Не подменяй insert ссылкой на CDN или draft. Не пропускай локальный файл.

Русская типографика

Весь русский текст, который ты пишешь в материалы Демонстратора (markdown уроков, подписи, :::important, тексты внутри ИИ-HTML), подчиняется русской типографике. Смысл не менять — только оформление.

Английский, код, URL, имена блоков :::тип — не трогать этими правилами.

Обязательно

ЧтоКак
Тире между словами / пояснениеДлинное (U+2014) с пробелами: Редактор — инструмент
ДефисТолько в словах и номерах без пробелов: онлайн-курс, какой-то
Кавычки«ёлочки»; вложенные — „лапки“
МноготочиеСимвол (U+2026), не три точки «на глаз» и не ... в прозе
Число и единицаНеразрывный пробел: 3 дня, 10–15 минут, 690 ₽
Сокращенияи т. д., т. п. — с неразрывными пробелами где нужно

Не ставь дефис - вместо тире между частями предложения. Не ставь прямые кавычки "..." в русском тексте.

Неразрывный пробел в материалах

В markdown материалов редактора:

Если среда агента плохо вставляет U+00A0 — хотя бы соблюдай тире, кавычки и многоточие; пробелы после коротких слов по возможности.

Где применять

КонтекстТипографика
Текст урока / блока / подписиОбязательно
Русский текст внутри :::ai_widget / локального .htmlОбязательно
Комментарии %%, код, JSON, CSS, латиницаНе применять «ёлочки» к коду

Антипаттерны

Чеклист перед сдачей русского текста