Перейти к содержанию

Перенести базу знаний Битрикс24: 5 часов с нуля, полчаса по инструкции

Пять ловушек, шесть шагов, промт и схема кода: как перенести базу знаний в Битрикс24. Пять часов — в основном борьба с инструментом, по инструкции — полчаса.

Старые базы знаний неудобны для ИИ — это первая причина, по которой я взялся за перенос. Новые базы мы и так пишем с помощью ИИ: даём контекст, агент дописывает. «База знаний 2.0» Битрикс24 (модуль note) работает с Markdown — такой текст агент нормально читает и дописывает, а первое поколение баз для этого не приспособлено.

Вторая причина — Битрикс24 развивает ИИ-инструменты помощи для сотрудников и внешних заказчиков. Уже можно подключить бота, который по базе знаний подсказывает сотрудникам и отвечает клиентам. Но работает он только с базой знаний 2.0: первое поколение так не умеет.

Получается, перенос — не разовая уборка. Дальше эти базы будут актуализироваться с помощью ИИ, а не вручную, как пришлось бы в первой версии.

С нуля перенос занял у меня пять часов. Не потому что столько работы: на другом портале ту же операцию я делал с помощью Claude примерно за 15 минут на базу и начинал с ожиданием «две базы — полчаса-час». Пять часов съела не база знаний, а инструмент:

Инструкция и промт ниже — чтобы вам не пришлось повторять мой путь: по правильному маршруту две базы переносятся за полчаса.

В этом кейсе перенос сделал ИИ-агент «Битрикс24 Коворк/Код»: прочитал исходный сайт, выгрузил статьи и структуру, создал новую базу и перенёс всё с сохранением вложенности. Две базы знаний, 43 статьи, 75 изображений и 39 видео — без ручного копирования и без потерь. Дальше — что предстояло сделать, на чём такие переносы обычно ломаются, как выстроить работу, готовый промт для агента и схема кода.

Что было и что хотели получить

Что было. База знаний компании жила в старом формате: страницы-лендинги, где меню и содержимое собраны из визуальных блоков. Формально это «сайт со статьями», фактически — вики, которую легко читать и почти невозможно поддерживать.

Что хотели получить. Единую базу в модуле «База знаний» Битрикс24: статьи с нормальным деревом разделов, встроенными картинками и видео, поиском и правами доступа — место, куда сотрудник идёт за инструкцией, а не блуждает по страницам сайта.

Что нужно было сохранить. Тексты — без потерь и без «пересочинения». Структуру: раздел → подраздел → статья. Вложения: изображения остаются внутри статей, видео — доступными. Исходный сайт — не трогать вообще: он остаётся рабочей копией на время проверки.

Масштаб: две базы, 43 статьи

Показатель«Системная работа»«Техподдержка»Итого
Статей34943
Разделов верхнего уровня16420
Изображений68775
Видео39039
Объём текста~121 000 знаков~11 000 знаков~132 000 знаков

Страниц в исходном сайте первой базы было 40, а в дерево новой базы вошло 34: шесть страниц оказались служебными и в перенос не поехали. Это не потеря, а часть методики — почему, разберём ниже.

Почему это задача для агента

Работа большая, но однообразная: 43 страницы, у каждой — набор блоков, часть из них текст, часть — картинки и видео. Человек на такой рутине ошибается предсказуемо: пропускает вложенный раздел, теряет картинку, копирует служебную страницу. Агент делает это одинаково на первой и на сорок третьей статье — и, что важнее, повторяемо: если что-то пошло не так, прогон можно повторить, а не восстанавливать вручную.

На чём такие переносы ломаются

Перенос базы знаний выглядит как «скопируй текст в новое место». На практике ломается в пяти местах — и каждое стоит либо потерянного контента, либо вечера ручной сверки.

Дерево не переносится плоским списком

Главная ловушка. В исходном сайте вложенность живёт в меню: раздел «Продажи» содержит «Воронки», «Договора», «Счета», «Акты» и ещё шесть подразделов; «Интеграции заявок» — пять инструкций по подключению. Если перенести статьи списком, все они станут равноправными, и база превратится в свалку: раздел и его содержимое визуально не отличаются.

Хуже того, порядок создания важен: у статьи нельзя указать родителя, которого ещё не существует. Значит, нельзя «сначала все статьи, потом разложить» — создавать надо строго сверху вниз, иначе подраздел уедет в корень и вложенность придётся восстанавливать вручную.

Служебные страницы шаблона уезжают в новую базу

В исходном сайте есть страницы, которые не являются знаниями: главная страница шаблона, страница результатов поиска, сайдбар с меню, «Article example». Формально это те же страницы, и агент по умолчанию потащит их в новую базу. В результате в чистой базе знаний появляются пустые и технические статьи — и первое впечатление у сотрудника испорчено.

В этом кейсе из 40 страниц первой базы в перенос не пошли 6: «Стартовая», «Результаты поиска», «Сайдбар», «Страница категории», «Страница статьи» и устаревшая версия «BI Отчётов». Их надо распознавать по признакам шаблона (служебная страница, код шаблона вида wiki-light/main) и исключать осознанно — с записью причины, а не «на глаз».

Не-текстовые блоки: пустая статья или потерянный контент

Страница собрана из блоков: заголовок, текст, картинка, видео, разделитель, карта, соцкнопки. Если читать «весь HTML страницы», в статью приедет мусор из вёрстки. Если читать только текстовые блоки — потеряются картинки и видео, а статья с инструкцией «нажмите кнопку на скриншоте» станет бесполезной.

Правильный путь: обходить блоки по порядку, текст конвертировать в разметку, а на месте картинки оставлять маркер и заменять его на реальное изображение после загрузки файла. Тогда картинка остаётся там, где была в тексте, а не «где-то в конце статьи».

Ссылки меню могут вести в никуда

Меню — это ссылки. В нём встречаются пункты, которые указывают на отсутствующие или удалённые страницы, а одна и та же страница может быть указана дважды (например, «BI Отчёты» отдельным пунктом и внутри другого раздела). Если идти по меню вслепую, агент либо упадёт на 404, либо создаст дубль статьи.

Поэтому дерево строится не «по ссылкам», а по факту существования страницы, с журналом решений: какие пункты пропущены и почему, какие страницы в меню не упомянуты и остались неперенесёнными.

Портал отвечает медленно — это норма

В этом кейсе запросы к порталу шли через прокси приложения: на портальных путях они занимают десятки секунд, и ответ в 70–100 секунд — штатное поведение, а не авария. Отсюда два следствия.

Первое: таймаут на такие вызовы надо ставить в минутах. Обрыв на 30–60 секундах выбрасывает ответ, который уже был в пути, и перенос «падает» на здоровом портале.

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

Чего ещё стоит ждать

Как выстроить работу

Вся методика сводится к одной идее: сначала выгрузить всё локально, потом строить в новой базе. Пока данные не лежат у вас файлами, вы работаете вслепую — любой сбой на середине означает, что половину уже не восстановить. Локальная копия превращает перенос в повторяемую операцию: упало — прогон повторяется с того же места.

  1. Прочитать структуру и тексты. Источник не менять. Собрать список страниц, найти блок с меню и разобрать его в дерево «раздел → подраздел → статья». Отдельно выгрузить блоки каждой страницы и их содержимое. Источник на этом этапе — только для чтения: в любой момент можно сравнить «до» и «после» или откатиться.
  2. Сохранить всё локально файлами. Дерево — в один JSON (структура и признак исключения), статьи — в папки по идентификатору: текст в Markdown плюс журнал блоков. Это источник истины для следующих шагов. Побочный эффект: у вас сразу появляется архив базы знаний, независимый от портала.
  3. Отфильтровать служебные страницы. Убрать шаблонные и технические страницы, записав причину исключения для каждой. Результат — чистое дерево, в котором остались только статьи, несущие знания.
  4. Создать целевую базу. Одна коллекция в модуле «База знаний» с понятным названием. Перед созданием проверить, нет ли уже базы с таким именем: перенос должен быть идемпотентным.
  5. Перенести дерево сверху вниз. Обход в порядке «родитель раньше детей»: создаём раздел, получаем идентификатор, создаём в нём подразделы, передавая идентификатор родителя. Только так вложенность сохраняется с первого раза. Текст сохраняется сразу при создании, картинки — отдельным шагом, с заменой маркеров на реальные изображения.
  6. Сверить и отчитаться. Формальная сверка, а не «на глаз»: дерево в новой базе сравнивается с локальным (число статей, совпадение родителей), проверяется, что тексты непустые, маркеры заменены, число картинок совпадает с источником, а размеры и типы файлов — те же. Итог — отчёт: сколько статей, картинок и видео перенесено, что исключено и почему.

Что это дало на практике. Две базы, 43 статьи, 75 изображений, 39 видео — сверка не нашла расхождений. Один прогон скрипта создал дубли из-за неверного разбора ответа, но именно потому, что перенос шёл скриптом с журналом, дубли удалили подтверждённой операцией и прогон повторили — без ручного разбора 34 статей. В ручном режиме такой сбой означал бы вечер работы.

Промт для агента

Ниже — готовый промт. Замените три плейсхолдера в угловых скобках и отправьте агенту Коворк/Код в новом чате. Промт сформулирован так, чтобы агент сам прошёл все шесть шагов методики и не оставил вам ручной сверки.

Перенеси базу знаний из старого формата в модуль «База знаний» Битрикс24.

Источник: сайт «<название сайта-источника>» (id сайта <id сайта>).
Цель: новая коллекция «<имя новой базы>» в модуле «База знаний».

Правила работы:
1. Источник только читай. Ничего в нём не меняй, не удаляй и
   не переименовывай.
2. Сначала выгрузи всё локально в файлы: дерево разделов (JSON) и
   текст каждой статьи (Markdown по папкам). Локальные файлы —
   источник истины для переноса.
3. Служебные и шаблонные страницы (главная, поиск, сайдбар, примеры
   статей) в перенос не включай. Для каждой исключённой страницы
   запиши причину.
4. Создай коллекцию с указанным именем. Если она уже существует —
   используй её, дубль не создавай.
5. Переноси строго в порядке «родитель раньше детей»: сначала раздел,
   затем его подразделы с указанием родителя. Вложенность должна
   совпасть с исходной.
6. Изображения переноси внутрь статьи, сохраняя место в тексте.
   Видео — ссылкой на источник. Текстовые блоки конвертируй в
   Markdown, не-текстовые (разделители, карты, соцкнопки)
   пропускай.
7. Перенос сделай идемпотентным: веди журнал «страница источника →
   статья в новой базе», чтобы повторный запуск не создавал дубли.
8. В конце сверь результат с локальным деревом: число статей,
   совпадение родителей, непустые тексты, число картинок.
   Отчитайся таблицей: сколько статей, разделов, картинок и видео
   перенесено, что исключено и почему, какие расхождения нашла
   сверка.

Если что-то в источнике неоднозначно (пункт меню ведёт на отсутствующую
страницу, одна страница указана дважды, статья практически пустая) —
не угадывай: прими решение, запиши его в отчёт отдельным пунктом
и продолжай.

Что уточнить под себя. Название сайта и его id — из адреса или карточки сайта. Имя новой базы — так, как её будут искать сотрудники. Если в базе есть разделы, которые переносить не нужно (архив, черновики), допишите их списком исключений в правиле 3.

Чего в промте нет и почему. В нём нет ни ключей, ни адресов прокси, ни упоминания конкретного инструмента: доступ к порталу уже настроен в окружении, а способ вызова агент выбирает сам. Промт описывает результат и ограничения, а не пошаговые команды — так он остаётся рабочим, даже если внутренняя механика изменится.

Пример кода

Если хотите не «попросить агента», а сделать перенос сами (или дать агенту каркас), вот схема вызовов. Она намеренно короткая: чтение источника, создание коллекции, обход дерева, сверка. Ключ и базовый адрес берутся из окружения — в коде их быть не должно.

// Ключ и адрес — из окружения приложения, никогда не в коде и не в браузере.
const BASE = process.env.BITRIX_API_BASE_URL;
const KEY = process.env.BITRIX_API_KEY;

async function api(method, path, body) {
  const res = await fetch(BASE + path, {
    method,
    headers: { "X-Api-Key": KEY, "Content-Type": "application/json" },
    body: body ? JSON.stringify(body) : undefined,
    // Портал отвечает десятками секунд — таймаут в минутах, а не в секундах.
    signal: AbortSignal.timeout(180_000),
  });
  const json = await res.json();
  if (!res.ok || json.error) {
    throw new Error(
      `${json.error?.code ?? res.status}: ${json.error?.message ?? "request failed"}`
    );
  }
  // Ответ прокси может быть обёрнут: разворачиваем data.data либо data.
  return json.data?.data ?? json.data ?? json;
}

// 1. Чтение источника: страницы и блоки. Только чтение.
const pages = await api("GET", "/pages?scope=KNOWLEDGE&limit=500");
const blocks = await api("GET", `/pages/${pageId}/blocks?scope=KNOWLEDGE`);
const content = await api(
  "GET",
  `/pages/${pageId}/blocks/${blockId}/content?scope=KNOWLEDGE`
); // -> { html }

// 2. Дерево: разбираем блок меню в «раздел → подраздел → статья».
//    Служебные страницы помечаем excluded с причиной, ссылки на несуществующие
//    страницы пропускаем. Результат сохраняем локально в tree.json.

// 3. Целевая коллекция. Существует — используем, нет — создаём.
const collection = await api("POST", "/note/collections", {
  name: "База знаний компании",
});

// 4. Перенос: строго сверху вниз, родитель до детей.
async function transfer(node, parentId) {
  const markdown = renderMarkdown(node); // блоки -> Markdown, картинки -> маркеры
  const doc = await api("POST", "/note/documents", {
    collectionId: collection.id,
    title: node.title,
    markdown,
    ...(parentId ? { parentId } : {}), // родитель уже создан — вложенность сохранится
  });
  ledger.set(node.pageId, doc.id); // журнал: повторный прогон не создаст дубль

  for (const file of node.images) {
    // картинки — после текста, с заменой маркеров
    await api("POST", `/note/documents/${doc.id}/files`, {
      fileName: file.name,
      fileContent: file.base64, // base64 читает скрипт с диска, не модель
    });
  }
  for (const child of node.children ?? []) await transfer(child, doc.id);
}

for (const root of tree.root) await transfer(root, null);

// 5. Сверка: дерево в базе против локального дерева.
const remote = await api("GET", `/note/collections/${collection.id}/documents`);
// Проверяем: число статей совпадает, у каждого документа тот же родитель,
// тексты непустые, маркеров не осталось, число картинок равно источнику.

Три вещи, на которые стоит посмотреть в этом коде.

AbortSignal.timeout(180_000) — не перестраховка. Портал на портальных путях отвечает 70–100 секунд; таймаут в 30 секунд «уронит» перенос на полностью здоровом портале.

json.data?.data ?? json.data ?? json — разворачивание ответа. Ровно на этом месте в реальном переносе появились 34 пустых дубля: скрипт читал идентификатор не из того уровня вложенности, получал undefined и создавал статью заново вместо того, чтобы заполнить существующую. Дубли удалили, разбор ответа исправили.

ledger.set(node.pageId, doc.id) — журнал переноса. Он защищает от дублей при повторном прогоне и потом используется для сверки вложений: списка файлов у статьи в API нет, поэтому сверять приходится по сохранённым идентификаторам.

Что в итоге

Перенос, который вручную занял бы дни, агент выполнил за два сеанса — по одному на базу. Почти всё время ушло на ошибки инструмента, а не на статьи: 43 статьи, 75 изображений, 39 видео перенесены, вложенность сохранена и проверена скриптом. Исходный сайт остался нетронутым — его можно сравнить или держать как архив.

Главный вывод не про инструменты, а про порядок: сначала выгрузить всё в файлы, потом строить. Дерево — сверху вниз. Служебные страницы — исключать осознанно. Результат — сверять, а не «вроде выглядит нормально». Эти четыре правила делают перенос базы знаний повторяемой операцией, а не разовым подвигом.

Если перенос упал на середине

  1. Откройте журнал: в нём видно, какие страницы уже получили идентификатор в новой базе. Повторный прогон продолжит с этого места, а не создаст всё заново.
  2. Проверьте статьи из журнала: убедитесь, что текст непустой, маркеров вроде <image-1> не осталось, а картинки на месте. Пустой текст обычно означает, что страницу прочитали только по текстовым блокам, а маркер — что не отработал шаг с файлами.
  3. Сверьте число статей, разделов и картинок с локальным деревом и только потом отключайте старый сайт. Пока он на месте, любое расхождение можно проверить по исходнику.

Если хотите повторить — возьмите промт из раздела «Промт для агента», подставьте название и id своего сайта и попросите агента отчитаться таблицей: по числам сразу видно, всё ли доехало.


Поделиться:

Предыдущая статья
Почему я ушёл с Дзена в свой блог, а не в Telegram