By Fedor Rychkov

March 29, 2026 04:22 PM

Платформа статей с ИИ из коробки: редактор, медиа, аналитика и Markdown для LLM-агентов

Обзор возможностей: серверные вызовы моделей без утечки ключей, структурированные подсказки для SEO и текста, генерация изображений и озвучки, учёт использования моделей и просмотров, а также выдача одной и той же публичной статьи в HTML для людей и в Markdown — с метаданными и оценкой токенов — для инструментов и ИИ-агентов.

null
AI generated Image

Зачем это нужно из коробки

Поставляемый стек закрывает типичный цикл работы редакции и площадки:

  • Редактор получает подсказки модели в контексте уже сохранённой статьи и ревизии — без отдельного «чата в браузере с ключом».

  • SEO и превью можно сгенерировать структурированно и применить в формы одним действием, тело статьи — отдельным потоком в Markdown с обратной записью в TipTap.

  • Картинки и озвучка проходят через тот же медиа-пайлнайн (CDN URL), что и загрузки автора — без ручной передачи файлов между сервисами.

  • Администратор видит агрегированный расход токенов по источникам и пользователям и отдельно — просмотры статей и ревизий.

  • Внешние системы и LLM-агенты получают тот же канонический URL /article/{slug}, что и читатель, но с заголовком Accept: text/markdown — в ответ приходит документ с YAML front matter, заголовком Content-Signal и числом токенов в x-markdown-tokens, без парсинга HTML и без отдельного «API для роботов».

Ниже — принципы, страницы интерфейса, маршруты API и места для иллюстраций и примеров ответов.


Принципы работы

Принцип

Смысл

Ключи только на сервере

LLM_API_KEY не попадает в клиент; доступны только флаги и списки моделей с GET /api/v1/llm/models.

Явное включение ИИ

NEXT_PUBLIC_LLM_ENABLED должна быть буквально true; иначе интерфейс и серверные проверки считают LLM выключенным.

Неизменяемый журнал usage

Каждый значимый вызов модели фиксируется в LlmUsageEvent с полем source (чат, аудит, SEO, превью, контент, TTS, картинки и т.д.).

Медиа единообразно

Сгенерированные изображения и mp3 озвучки загружаются через существующий поток медиа; в UI — обычные URL.

Публичная статья — два представления

HTML и Markdown различаются по Accept; кэш учитывает Vary: Accept, чтобы CDN не смешивал ответы.

Переменные окружения для LLM и лимитов чата описаны в config/env.ts (LLM_CONFIG) и в .env.example: LLM_API_KEY, NEXT_PUBLIC_LLM_ENABLED, LLM_CHAT_MODELS, LLM_IMAGE_MODELS, LLM_CHAT_RATE_LIMIT_POINTS, LLM_CHAT_RATE_DURATION_SEC, а также APP_INTERNAL_ORIGIN для корректных серверных запросов к своим API (например за reverse proxy или в Docker) при выставлении Content-Signal на HTML-странице статьи.



Один URL — HTML для людей, Markdown для агентов

Для опубликованной публичной статьи канонический адрес остаётся **/article/{slug}**. Браузер запрашивает HTML как обычно. Клиент, который передаёт **Accept: text/markdown** (с приоритетом выше, чем у text/html), получает не HTML, а текст Markdown с YAML front matter в начале файла: заголовок, описание, slug, canonical, язык, флаг обучения (allow_ai_training), даты публикации и обновления — затем тело статьи в Markdown, согласованное с тем же рендером контента, что используется для HTML.

Практическая польза для LLM и интеграций:

  • не нужно тянуть HTML, чистить теги и терять структуру;

  • один запрос даёт сжатый по токенам текст плюс метаданные в машиночитаемом виде;

  • заголовок **x-markdown-tokens** даёт оценку объёма документа в токенах (подсчёт через gpt-tokenizer, профиль o200k_base) — удобно для бюджетирования вызовов к своей или внешней модели;

  • заголовок **Content-Signal** передаёт политику в духе Content Signals: например, разрешено ли использование материала для обучения (ai-train=yes|no) при сохранённых search=yes, ai-input=yes для публичной выдачи.

Технически запрос обрабатывается в **src/proxy.ts**: при подходящем Accept выполняется rewrite на внутренний маршрут GET /api/v1/public/article/markdown (slug передаётся через query и служебный заголовок). Для HTML-ответа Content-Signal подмешивается отдельным запросом к GET /api/v1/public/article/content-signal?slug=... с базой APP_INTERNAL_ORIGIN, если публичный origin запроса ненадёжен.

Примеры вызовов (curl)

Запрос Markdown по публичному URL статьи (как это сделает агент или скрипт):

curl -sS -D - -o /tmp/article.md \
  -H "Accept: text/markdown;q=1.0, text/html;q=0.5" \
  "https://example.com/article/my-article-slug"

В ответе ожидаются среди прочего:

  • Content-Type: text/markdown; charset=utf-8

  • Vary: Accept

  • Content-Signal: ai-train=yes, search=yes, ai-input=yes (значения зависят от настроек статьи)

  • x-markdown-tokens: <целое число>

Прямой вызов внутреннего API (удобно для документации и отладки; на продакшене может отличаться базовый URL и rate limit):

curl -sS -D - -o /tmp/article.md \
  "https://example.com/api/v1/public/article/markdown?slug=my-article-slug"

Пример JSON от content-signal (для вставки в статью)

Эндпоинт GET /api/v1/public/article/content-signal?slug=... возвращает JSON с готовой строкой для заголовка Content-Signal:

{
  "contentSignal": "ai-train=yes, search=yes, ai-input=yes"
}

При allowAiTraining: false у статьи в ответе будет ai-train=no.

Пример фрагмента тела ответа Markdown (YAML + начало текста)

Ниже — иллюстративный фрагмент формата (поля front matter соответствуют buildPublicArticleMarkdownDocument):

---
title: "Заголовок статьи"
description: "Краткое описание для превью и агентов"
slug: "my-article-slug"
canonical: "https://example.com/article/my-article-slug"
language: "ru"
allow_ai_training: yes
published_at: "2026-03-01T12:00:00.000Z"
updated_at: "2026-03-15T09:30:00.000Z"

Сравнение: HTML vs Markdown для одного slug

Критерий

HTML (Accept по умолчанию у браузера)

Markdown (Accept: text/markdown приоритетнее HTML)

Тип контента

text/html

text/markdown; charset=utf-8

Удобство для парсинга агентом

низкое (разметка, шум)

высокое (структура, меньше токенов на смысл)

Метаданные

в основном в и разметке

в YAML сверху файла

Оценка объёма для LLM

нестандартизирована

заголовок x-markdown-tokens


Редактор: стриминг чата, аудит, структурированные подсказки

Чат

POST /api/v1/llm/chat/stream — поток SSE; контекст собирается из заголовка статьи, SEO-полей и plain text из TipTap (extractPlainTextFromRevisionContent). Доступ только ролям ADMIN / EDITOR; действует отдельный rate limit на пользователя. События учёта: source: chat_stream. История подгружается через GET /api/v1/llm/chat/history (сущности LlmChatSession, LlmChatMessage). Точки входа ИИ в UI доступны после сохранения статьи с ревизией.

{
  "enabled": true,
  "chat": { "models": [{ "id": "gpt-4o-mini", "label": "GPT-4o mini" }] },
  "audit": { "models": [{ "id": "gpt-4o-mini", "label": "GPT-4o mini" }] },
  "image": {
    "models": [
      {
        "id": "gpt-image-1",
        "label": "GPT Image 1",
        "defaultAspectRatioId": "16:9",
        "aspectRatios": [{ "id": "16:9", "label": "16:9", "size": "1536x1024" }]
      }
    ]
  }
}

Аудит статьи

POST /api/v1/llm/article-audit — JSON-режим; результат сохраняется в LlmArticleAudit, список — GET /api/v1/llm/article-audit. В типах ответа — секции preview, content, seo с оценками, сильными сторонами, замечаниями и рекомендациями.

{
  "audit": {
    "preview": {
      "score": 8,
      "summary": "Превью соответствует теме.",
      "strengths": ["Ясный лид"],
      "issues": ["Длинное второе предложение"],
      "recommendations": ["Сократить подзаголовок"]
    },
    "content": { "summary": "…", "strengths": [], "issues": [], "recommendations": [] },
    "seo": { "summary": "…", "strengths": [], "issues": [], "recommendations": [] },
    "overall": "Готово к публикации с правками превью."
  },
  "usage": { "promptTokens": 1200, "completionTokens": 400, "totalTokens": 1600 },
  "model": "gpt-4o-mini",
  "savedId": "67d…"
}

SEO, превью и тело (структурированные ответы API)

Маршрут

Назначение

Ключевые поля в JSON

POST /api/v1/llm/seo/suggest

Мета и OG

metaTitle, metaDescription, ogTitle, ogDescription, keywords

POST /api/v1/llm/preview/suggest

Карточка превью

title, description, rationale

POST /api/v1/llm/content/suggest

Тело статьи

markdown, rationale

Пример ответа SEO (поля SeoSuggestApiResponse):

{
  "suggest": {
    "metaTitle": "Новая статья: введение в платформу",
    "metaDescription": "Кратко о возможностях редактора и публичной выдачи.",
    "ogTitle": "Новая статья — платформа",
    "ogDescription": "То же для соцсетей.",
    "keywords": "платформа, статьи, markdown, seo"
  },
  "usage": { "promptTokens": 800, "completionTokens": 200, "totalTokens": 1000 },
  "model": "gpt-4o-mini"
}

Пример ответа контента (ContentSuggestApiResponse):

{
  "suggest": {
    "markdown": "## Введение\n\nТекст сгенерированной статьи в Markdown…",
    "rationale": "Структура: введение, три раздела, вывод."
  },
  "usage": { "promptTokens": 2000, "completionTokens": 1500, "totalTokens": 3500 },
  "model": "gpt-4o-mini"
}

Генерация изображений

Маршруты: POST /api/v1/llm/image/generate, .../image/generate/stream, .../image/prompt/stream. Результат — медиа-ассет и proxyUrl, плюс promptUsed и usage. В интерфейсе — компонент MediaUrlUploadField (превью статьи, OG, диалог картинки в редакторе).

{
  "asset": { "id": "…", "mimeType": "image/png" },
  "proxyUrl": "https://…",
  "usage": { "promptTokens": 0, "completionTokens": 0, "totalTokens": 1000 },
  "model": "gpt-image-1",
  "aspectRatioId": "16:9",
  "promptUsed": "иллюстрация к статье про платформу, плоский стиль"
}

Озвучка (TTS)

POST /api/v1/article/listen-audio/generate — генерация mp3, запись полей listenAudioAssetId и связанных на Article. Учёт: source: listen_tts (в числовых полях — символы текста для сопоставления с биллингом TTS).


Просмотры статей

POST /api/v1/article/view — инкремент счётчиков на Article и опубликованной ArticleRevision; дедупликация через Redis ~24 ч на связку посетитель/пользователь + статья + ревизия, если Redis доступен. Админка: GET /api/v1/article/views/dashboard, GET /api/v1/article/views/by-article/[articleId], страница /admin/article-views.


Учёт использования LLM

Журнал LlmUsageEvent с перечислением источников: chat_stream, article_audit, seo_suggest, preview_suggest, content_suggest, listen_tts, image_generate, image_prompt_stream, image_prompt_article.

  • GET /api/v1/llm/usage/dashboard?days= — сводка для админов (/admin/llm-usage).

  • GET /api/v1/llm/usage/article?articleId=&revisionId= — сводка по статье/ревизии для редактора.

{
  "since": "2026-03-01T00:00:00.000Z",
  "until": "2026-03-27T23:59:59.999Z",
  "days": 14,
  "totals": {
    "eventCount": 420,
    "totalPromptTokens": 900000,
    "totalCompletionTokens": 300000,
    "totalTokens": 1200000
  },
  "bySource": [
    { "source": "chat_stream", "count": 200, "totalTokens": 400000 },
    { "source": "seo_suggest", "count": 50, "totalTokens": 80000 }
  ],
  "topUsers": [{ "userId": "…", "eventCount": 80, "totalTokens": 200000 }],
  "recent": []
}

Сводная таблица HTTP API

Область

Метод

Путь

Чат

POST

/api/v1/llm/chat/stream

Чат

GET

/api/v1/llm/chat/history

Модели

GET

/api/v1/llm/models

Аудит

POST, GET

/api/v1/llm/article-audit

SEO / превью / контент

POST

/api/v1/llm/seo/suggest, /api/v1/llm/preview/suggest, /api/v1/llm/content/suggest

Картинки

POST

/api/v1/llm/image/generate, /api/v1/llm/image/generate/stream, /api/v1/llm/image/prompt/stream

Usage

GET

/api/v1/llm/usage/dashboard, /api/v1/llm/usage/article

TTS

POST

/api/v1/article/listen-audio/generate

Просмотры

POST

/api/v1/article/view

Просмотры (админ)

GET

/api/v1/article/views/dashboard, /api/v1/article/views/by-article/[articleId]

Публичное: Markdown

GET

/api/v1/public/article/markdown

Публичное: сигнал

GET

/api/v1/public/article/content-signal


Итог

Платформа даёт единый контур для редакции: ИИ встроен в сохранённый документ, структурированные ответы стыкуются с формами и редактором, медиа и озвучка не выбиваются из CDN-потока, расход моделей и просмотры прозрачны в админке. Отдельно выделяется публичная выдача Markdown по согласованию содержимого на том же URL, что и HTML: это снижает стоимость токенов для downstream-LLM, упрощает интеграции и сочетается с заголовками **Content-Signal** и **x-markdown-tokens** — без дублирования «человеческой» и «машинной» версии сайта на разных адресах.


Дополнительные материалы в репозитории

Подробные чеклисты реализации и планов: docs/AI_FEATURES_ROADMAP.md, docs/PRODUCT_ROADMAP.md.

Скриншоты интерфейса, дашбордов и так далее

Общий вид страницы редактирования/просмотра статьи
Общий вид страницы редактирования/просмотра статьи
Форма работы с ИИ помощниками
Форма работы с ИИ помощниками
Процесс аудита текущего состояния ревизии статьи
Процесс аудита текущего состояния ревизии статьи