Ворклог по задаче "Отформатировать контент"

11 сент. 2026 г., 10:50:07

Зафиксирован прогресс по автоматическому восстановлению форматирования старого контента после миграции из draft-editor.

Написан резолвер, который берет старые варианты контента (contentText и content) и через ИИ-агента пересобирает публикацию в Markdown с сохранением исходных слов, смысла и стилистики.

import { Resource } from '@prisma/client'
import { PrismaContext } from 'server/context/interfaces'
import {
  LLMChatMessageRole,
  LlmModel,
  LlmProvider,
} from 'server/llm/client/interfaces'
import { llmChatCompletionResolver } from 'server/schema/types/LLM/resolvers/chatCompletion'

type ProcessConceptsArgs = {
  ctx: PrismaContext
  resource: Resource
}

export async function processOldContent({
  ctx,
  resource,
}: ProcessConceptsArgs): Promise<string | null | undefined> {
  const systemPrompt = `# Ты рерайтер.

Тебе будут даны разные варианты текста одной и той же публикации. Это может быть и чистый текст, и технический со всякими техническими тегами и т.п. Твоя задача пересобрать полностью статью, с сохранением слов, смысла и стилистики, но оформить все в формате маркдаун.

Так же могут быть приведены листинги кода, их надо вставить в блок кода с указанием языка. Например

\`\`\`js
const foo = 0
\`\`\`

Список всех поддерживаемых языков: 
'': 'Plain Text',
text: 'Plain Text',
js: 'JavaScript',
ts: 'TypeScript',
tsx: 'TypeScript (React)',
jsx: 'JavaScript (React)',
css: 'CSS',
html: 'HTML',
json: 'JSON',
bash: 'Bash',
shell: 'Shell',
sql: 'SQL',
gql: 'GraphQL',
python: 'Python',
markdown: 'Markdown',
django: 'Jinja/Django',
env: 'Environment',
yaml: 'YAML',

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


## !!! Очень важно !!! 

Важно сохранить смысл, слова и стилистику. Твоя задача отформатировать статью, а не переписать ее своими словами. Нельзя ничего додумывать, придумаывать и т.п. Если где-то есть недосказанность, эта недосказанность так и должна остаться - твоя задача только отформатировать статью.

Вернуть надо только отформатированную статью в формате маркдаун, без какого-либо технического сопроводительного кода, текста, твоих комментариев и т.п. Только сама статья. 


## Форматирование конечной публикации

Корректный Markdown с поддержкой html (но предпочтительней именно маркдаун форматирование)

Важно учитывать, что много где пропущены пробелы в тексте. Очень важно распознавать такое и корректно вставлять пробелы.

Картинки вставляются как картинки с предпросмотром. Ссылки как ссылки.

Если встречаются ссылки, ссылающиеся на домен community.modx-cms.ru, то вставляй просто текстом, а не ссылкой.
На другие домены вставляй ссылки как есть. 

Если есть картинки с внешних файлообменников типа joxi и т.п., не надо их вставлять, они не актуальны. Локальные картинки обязательно вставляй, где они указаны.
`

  const {
    content,
    contentText,

    /**
     * contentV2 не берем, потому что он формировался из content и contentText
     */
    // contentV2
  } = resource

  const peaces = [contentText, content]
    .map((n) =>
      n === null ? null : typeof n === 'string' ? n.trim() : JSON.stringify(n),
    )
    .filter((n) => !!n)

  if (!peaces.length) {
    return ''
  }

  const userPrompt = `## Вот тебе исходные данные статьи. Перепиши это в конечную статью в формате маркдаут, с сохранением слов, смысла и стилистики, и верни мне только ее, без всякого дополнительного технического текста и твоих комметариев

---

${peaces.join(`

---

`)}
  
`

  const chatResponse = await llmChatCompletionResolver(
    null,
    {
      input: {
        provider: LlmProvider.OpenRouter,
        messages: [
          {
            role: LLMChatMessageRole.system,
            content: systemPrompt,
          },
          {
            role: LLMChatMessageRole.user,
            content: userPrompt,
          },
        ],
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        model: LlmModel.GEMINI_3_5_FLASH_LITE as any,
      },
    },
    ctx,
  )

  const responseContent = chatResponse.choices?.[0]?.message?.content

  return responseContent
}

Статистика выполнения:

  • 100 публикаций — около 5 минут, бюджет ~$0.30.
  • 1296 ресурсов — около 43 минут, бюджет $2.53.

В целом обработка отработала нормально: текст стал заметно более удобочитаемым. До этого после миграции из draft-editor значительная часть публикаций превратилась фактически в сплошной текст без исходного форматирования.

При этом автоматическая обработка не дает полностью чистый результат и оставляет отдельные артефакты. Например, встречается такой результат:

В create- процессоре она есть, а в update- процессоре ее нет. Уточню, что это ключ массива данных, передаваемых в массив 
s
c
r
i
p
t
P
r
o
p
e
r
t
i
e
s
,
доступныйвнавешиваемыхплагинах
.
Приэтомониещеисоветуютэтотключнеиспользовать
,
ииспользуйтетипаключ
scriptProperties,доступныйвнавешиваемыхплагинах.Приэтомониещеисоветуютэтотключнеиспользовать,ииспользуйтетипаключthis->objectType.

Исходный HTML этого предложения:

<p>В create- процессоре она есть, а в update- процессоре ее нет. Уточню, что это ключ массива данных, передаваемых в массив <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>s</mi><mi>c</mi><mi>r</mi><mi>i</mi><mi>p</mi><mi>t</mi><mi>P</mi><mi>r</mi><mi>o</mi><mi>p</mi><mi>e</mi><mi>r</mi><mi>t</mi><mi>i</mi><mi>e</mi><mi>s</mi><mo separator="true">,</mo><mtext>доступныйвнавешиваемыхплагинах</mtext><mi mathvariant="normal">.</mi><mtext>Приэтомониещеисоветуютэтотключнеиспользовать</mtext><mo separator="true">,</mo><mtext>ииспользуйтетипаключ</mtext></mrow><annotation encoding="application/x-tex">scriptProperties, доступный в навешиваемых плагинах. При этом они еще и советуют этот ключ не использовать, и используйте типа ключ </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8778em;vertical-align:-0.1944em"></span><span class="mord mathnormal" style="margin-right:0.0278em">scr</span><span class="mord mathnormal">i</span><span class="mord mathnormal" style="margin-right:0.1389em">ptP</span><span class="mord mathnormal" style="margin-right:0.0278em">r</span><span class="mord mathnormal">o</span><span class="mord mathnormal">p</span><span class="mord mathnormal" style="margin-right:0.0278em">er</span><span class="mord mathnormal">t</span><span class="mord mathnormal">i</span><span class="mord mathnormal">es</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord cyrillic_fallback">доступныйвнавешиваемыхплагинах</span><span class="mord">.</span><span class="mord cyrillic_fallback">Приэтомониещеисоветуютэтотключнеиспользовать</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em"></span><span class="mord cyrillic_fallback">ииспользуйтетипаключ</span></span></span></span>this-&gt;objectType.</p>

Здесь проблема уже заложена в старом HTML: обычный текст оказался завернут в KaTeX/MathML, из-за чего модель пытается интерпретировать сильно искаженную структуру и местами дублирует/разбивает текст.

Вывод: автоматическая обработка существенно улучшает базовое состояние контента и подходит как массовый первый проход, но финальная вычитка и небольшие ручные правки все равно нужны.

Объем — около 1500 топиков за 13 лет. С учетом того, что большая часть после обработки визуально уже вполне читабельна и ручного внимания требуют в основном отдельные артефакты и местами сложная верстка, объем финальной ручной работы выглядит приемлемым.

26.08.2026

Исправить плохое оформление страниц после некачественного обновления сайта.