Обычные формы 1С в агентном пайплайне: пошаговая распаковка
Статья разбирает конкретную техническую проблему: обычные формы 1С хранятся в бинарном формате Form.bin, и стандартный агентный пайплайн их не видит - RAG не индексирует, git diff показывает «binary file changed», агент не может ни прочитать структуру формы, ни провести code-review правок. Рассмотрены два open-source распаковщика - saby-integration/v8unpack (Python, MIT) и e8tools/v8unpack (C++, MPL-2.0) - со сравнением по форматам, лицензиям и способу интеграции. Поверх описаны три доработки: фабрика путей, обработка частичной распаковки через флаг extraction_ok, контроль рассинхрона через forms_index. Отдельно - работа с .erf-отчётами и извлечение запросов СКД из Template.bin. В конце чек-лист и раздел «Что бы я сделал иначе». Все примеры синтетические, код - Python.
Form.bin. Вот точка, где красивое «да просто подключим LLM к репозиторию» разбивается о реальность. Платформа держит обычные формы в собственном бинарном формате, и по этому файлу не работает вообще ничего из привычного агентного пайплайна: ни RAG (Retrieval-Augmented Generation - метод дополнения LLM контекстом из индекса), ни git-diff, ни «прочитай и поправь». Слепое пятно.
Я упёрся в это на ровном месте. Собрал индекс, агент бодро отвечает по модулям - а формы как будто не существуют. Полез смотреть почему, и там бинарник.
В статье - рабочий подход, который я в итоге собрал: берём open-source распаковщик, добавляем поверх точечные доработки под кейс агента, встраиваем в индексацию. Без запуска платформы 1С.
Коротко: что делать
-
Стандартная выгрузка конфигурации даёт по каждой обычной форме файл Form.bin в формате платформы. LLM-агент его не читает, RAG не индексирует, git показывает «binary file changed».
-
Задача давно решена сообществом. Велосипед изобретать не надо. Берём готовый v8unpack: saby-integration/v8unpack - Python, MIT (лицензия Массачусетского технологического института), pip install v8unpack; либо e8tools/v8unpack - C++ gcc-порт, MPL-2.0 (Публичная лицензия Mozilla, версия 2.0).
-
Поверх распаковщика - трёхэтапный пайплайн:
1. распаковка Form.bin в человеко-читаемые Form/<имя>/Form.obj.bsl и Ext/ObjectModule.bsl;
2. индекс forms_index - реестр статуса распаковки и карта свежести для .epf/.erf;
3. распаковка как предварительный шаг перед индексацией для RAG: index_cf раскладывает все формы, и code_context() видит их код.
Диаграмма пайплайна:

Проблема: почему стандартная выгрузка не годится для LLM
Стандартная выгрузка конфигурации в файлы даёт по каждой обычной форме такую структуру:
.../Forms/<ИмяФормы>/Ext/Form.bin
.../Forms/<ИмяФормы>/Ext/Form/Module.bsl # модуль формы в формате BSL (Built-in Script Language - встроенный язык 1С) - текст, всё хорошо
С Module.bsl проблем нет. Это обычный BSL, его читает кто угодно - и человек, и агент. А вот Form.bin - внутренний бинарный формат платформы. В нём лежит:
-
разметка реквизитов/команд/таблиц формы;
-
иерархия элементов (группы, страницы, панели);
-
привязки к данным;
-
описание обработчиков (имена процедур из Module.bsl).
Для агента это глухо по всем фронтам:
| Что хотим | Почему не работает на `Form.bin` |
|---|---|
| RAG-индекс по формам | бинарник не разбивается на смысловые чанки |
| git diff при ревью PR | git показывает «binary file changed», без контекста |
| Семантический поиск «где обрабатывается команда X» | команда X - только в бинарнике |
| Автоправка элементов формы | агент физически не видит структуру |
| Code-review правок форм | reviewer (живой или агент) тоже видит только бинарник |
Я честно потратил полдня, прежде чем признал: без распаковки тут ловить нечего.
Два рабочих open-source распаковщика
Оба распаковывают без запуска платформы 1С. Это критично - и для CI (Continuous Integration - автоматическая сборка и проверка), и для агента, который крутится на машине, где платформы нет вообще.
saby-integration/v8unpack - Python, MIT, pip install v8unpack. Поддерживает .cf, .cfe, .epf. Поддержка .erf включена: [PR #29](https://github.com/saby-integration/v8unpack/pull/29) принят, изменения доступны через `pip install v8unpack`. Ставится через PyPI, вызывается двумя строчками:
import v8unpack
v8unpack.extract("source.cf", "unpacked/")
v8unpack.build("unpacked/", "rebuilt.cf")
Структура на выходе человеко-читаемая: имена объектов, BSL отдельными файлами, JSON-описания метаданных. Картинки и двоичные макеты остаются как есть.
e8tools/v8unpack - C++ gcc-порт, MPL-2.0. Поддерживает .cf, .epf, .erf. Ставится из системных пакетов Linux. Удобен, когда Python нежелателен или нужна максимальная скорость на тяжёлых выгрузках.
| Критерий | saby (Python) | e8tools (C++) |
|---|---|---|
| Установка | pip install v8unpack | системные пакеты Linux |
| Расширения | cf, cfe, epf, erf* | cf, epf, erf |
| Лицензия | MIT | MPL-2.0 |
| Интеграция в Python-агент | import v8unpack - нативная | subprocess.run(["v8unpack", ...]) - внешний процесс |
| Выходная структура | человеко-читаемая (имена, JSON, BSL) | плоская (исторический формат) |
* поддержка .erf включена в pip-релиз начиная с версии 1.2.10 - см. [PR #29](https://github.com/saby-integration/v8unpack/pull/29), принят.
Про subprocess: e8tools/v8unpack - это C++-программа, не Python-библиотека. Из Python её нельзя сделать import. Запускается как внешняя команда: subprocess.run(["v8unpack", "-E", "source.cf", "unpacked/"]) - Python вызывает программу как команду в терминале и ждёт результата.
Базовая рекомендация: для агента на Python берём saby. Встраивается одним импортом, покрывает .cf/.cfe/.epf; .erf доступен через PR/patch, пока не в pip-релизе. Структура выхода уже близка к тому, что нужно индексировать. e8tools держу в резерве - для экстремально больших выгрузок или когда Python нежелателен.
Пофайловая выгрузка vs. .cf-файл - что подаётся в v8unpack
И вот тут меня ждали первые грабли. Получить файлы конфигурации можно двумя способами, и они не равнозначны.
Способ А - пофайловая выгрузка (Конфигурация -> Выгрузить конфигурацию в файлы...): даёт дерево папок с Form.bin внутри каждой формы. Это не вход для v8unpack. Отдельный Form.bin из такой выгрузки - несжатый v8-контейнер (сигнатура ffffff7f), и v8unpack.extract() падает с ошибкой zlib.error: invalid block type. Небольшая заминка, пока не дошло, что подаётся не тот формат.
Способ Б - .cf-файл (Конфигурация -> Сохранить конфигурацию в файл...): даёт один .cf. Вот это правильный вход - v8unpack распаковывает его полностью, раскладывая все формы в Form/<ИмяФормы>/Form.obj.bsl.
| Что есть | Вход для v8unpack | Результат |
|---|---|---|
| Пофайловая выгрузка (.../Ext/Form.bin) | нет - несжатый контейнер | zlib.error |
| .cf-файл | да | Form.obj.bsl по каждой форме |
| .epf / .erf | да | BSL + макеты / BSL + Template.bin |
Практическое правило: если в пайплайне пофайловая выгрузка для git-хранения - держи рядом процесс сборки .cf-файла специально под прогон v8unpack. Либо работай напрямую с .epf/.erf для внешних обработок и отчётов - они автономные файлы-контейнеры.
EDT и управляемые формы - другой сценарий
Если ты работаешь в EDT, картина другая. EDT хранит конфигурацию как дерево файлов на диске - по одному файлу на объект. Управляемые формы лежат примерно так:

Form.form - это XML, не бинарник. v8unpack здесь не нужен вообще - агент читает XML напрямую. Но именно в EDT forms_index с mtime работает честно: разработчик поменял одну форму - на диске обновился один Form.form. Пайплайн смотрит mtime пофайлово и переиндексирует только изменённую форму, не трогая остальные 200+. Это настоящая инкрементальная индексация - в отличие от .cf-файла, где v8unpack.extract() всегда пересобирает всё целиком.
Итог по сценариям: для .cf-файла - forms_index это реестр статуса распаковки (была ли вообще, полная или частичная). Для .epf/.erf и EDT Form.form - это честный детект изменений по mtime с инкрементальной переработкой.
.erf и схема компоновки данных
.epf (внешняя обработка) и .erf (внешний отчёт) идентичны по контейнерной структуре - v8unpack распаковывает оба одинаково. Но для агента это не одно и то же. Внутри .erf почти всегда живёт схема компоновки данных (СКД) - встроенный в 1С механизм построения отчётов, и именно там прячется вся нетривиальная логика.
Что даёт распаковка .erf через v8unpack:

BSL-код агент уже читает. А вот сам запрос СКД - массив наборов данных, связи, вычисляемые поля, условное оформление - лежит в Template.bin как XML внутри v8-контейнера. v8unpack его не разбирает. Ещё один сериализованный слой внутри уже распакованного текстового слоя. Матрёшка.
Практическое наблюдение: Template.bin содержит XML внутри v8-контейнера. Текстовые запросы СКД достаются простым regex по ВЫБРАТЬ без зависимостей - полноценный бинарный парсер тут не нужен. Да, regex вместо честного парсера. Знаю. Но для индексации этого хватает.
Почему это важно для агента:
| Что агент хочет сделать | Только BSL (после v8unpack) | BSL + извлечённые запросы СКД |
|---|---|---|
| Ревью кода модуля отчёта | да | да |
| Найти «в каком отчёте есть запрос к регистру X» | нет | да |
| Объяснить логику группировок/фильтров | нет | да |
| Найти вычисляемые поля с нестандартной формулой | нет | да |
| Code-review изменений в структуре наборов данных | нет (git видит бинарник) | да |
Двухэтапная схема для .erf:

extract_skd_queries.py - это второй шаг, специфичный для отчётов. Работает уже с распакованной директорией и вытаскивает строки запросов СКД из Template.bin в читаемый JSON. Шаг некритичен. Если скрипт вернул ненулевой код (нестандартная сериализация, новая версия платформы) - BSL уже выгружен и доступен агенту, а SKD-шаг просто не прерывает цикл.
Пример вывода skd_queries.json:

Практическое правило: для .epf хватает v8unpack. Для .erf - v8unpack плюс отдельный шаг извлечения СКД. В FormArtifact стоит завести поле skd_extracted: bool, чтобы агент знал, доступен ли ему полный контекст отчёта или только BSL.
Доработки поверх распаковщика
«Просто вызвать extract()» - недостаточно. Я это понял быстро. Чтобы сырой вывод extract() стал полноценным входом для агента, добавил три доработки.
1. Фабрика путей по конвенции. На выходе extract() структура заточена под человека и git, но не под индексацию для RAG. Цель - чтобы по имени формы вычислить путь к её текстам без обхода файловой системы. Паттерн реализован в v8unpack-agent; пример вызова:

Расположение файлов зафиксировано в одном месте - в фабрике путей. Изменится структура выгрузки - правка нужна только здесь. Индекс и агента трогать не придётся.
Примечание по конвенции путей: unpacked_root в form_paths указывает на папку конкретного объекта (например unpacked_cf/DataProcessor/АдреснаяКнига), а не на корень всей выгрузки. Для .erf конвенция другая: форма лежит в ReportForm/<ИмяФормы>/ReportForm.obj.bsl вместо Form/<ИмяФормы>/Form.obj.bsl.
2. Обработка частичной распаковки. Часть форм может распаковываться не полностью: вложенные панели, нестандартные элементы, артефакты совместимости. Распаковщик при этом не падает - отдаёт что смог. Это надо явно фиксировать в метаданных:

extraction_ok=False - это не ошибка пайплайна, а сигнал агенту: «по этой форме видна только часть, не делай выводов о полноте».
3. Form.obj.bsl - это только модуль, не структура. После распаковки агент получает Form.obj.bsl с кодом обработчиков. Но само дерево элементов - реквизиты, группы, вложенные группы, кнопки, страницы Panel'а, привязки к данным - лежит в отдельном файле CatalogForm.elem.json (или аналогичном по объекту). Агент, читающий только Form.obj.bsl, видит процедуры-обработчики, но не видит структуру формы: какие кнопки есть, в какой группе вложены, к каким реквизитам привязаны поля. Это второй артефакт формы, который нужно индексировать отдельно. Подход к его обработке - тема следующей статьи.
Текстовый слой и индекс распаковки
Распаковка - это только этап 1. Без реестра статуса агент не знает, что уже распаковано, что распаковалось частично, а что вообще не трогалось.
Важное уточнение про mtime и .cf-файл. v8unpack.extract() не поддерживает частичную распаковку - он всегда пересобирает всё из .cf-файла целиком. Поэтому для .cf-файла детект «протухших» форм по mtime отдельных Form.bin смысла не имеет: либо ты запустил extract() и все формы свежие, либо нет. forms_index здесь - реестр статуса распаковки (была ли вообще, полная или частичная), а не детект изменений.
Для .epf/.erf картина другая - каждый файл независим, и mtime честно трекает изменения. В EDT mtime по Form.form работает ещё честнее - там вся конфигурация уже пофайловая.
forms_index: реестр распакованных форм
Это JSON-файл рядом с выгрузкой, с записями по каждой форме:

Тут сразу видны два характерных случая. ФормаСписка - распакована полностью и актуальна. ФормаЭлемента - распакована частично (extraction_ok: false) и bin_mtime > unpacked_mtime: это значит .cf-файл был перезаписан после последнего extract(), и при следующем запуске пайплайн перераспакует всё заново.
Описание полей:
| Поле | Тип | Назначение |
|---|---|---|
| bin_path | string | Путь к исходному Form.bin относительно корня выгрузки |
| unpacked_root | string | Путь к директории с распакованными текстами |
| bin_mtime | float | Unix-время последнего изменения Form.bin |
| unpacked_mtime | float | Unix-время последней распаковки; если bin_mtime > unpacked_mtime - нужен повторный extract() |
| extraction_ok | bool | true - распаковка полная; false - частичная, агент учитывает неполноту |
| warnings | array | Диагностика при частичной распаковке; пустой массив если extraction_ok: true |
Индекс - не источник истины. Источник это Form.bin в выгрузке. Индекс - карта состояния: по нему видно, для каких форм распаковка была, была ли полной, и не устарел ли .cf-файл с момента последнего extract().
Детект рассинхрона через MCP (Model Context Protocol) filesystem
Перед тем как агент «прочитает» текст формы, MCP-инструмент сравнивает mtime:

Эта проверка актуальна для .epf/.erf и EDT - там каждый файл независим и mtime честно отражает изменения. Для .cf-файла логика другая: если .cf был перезаписан - is_form_stale вернёт True по всем формам разом (они все обновились вместе с .cf). Пайплайн запустит extract() заново и обновит индекс целиком.
Что кладётся в индекс
В индекс кладём только то, что нужно для маршрутизации к текстам и проверки свежести. Никакого содержимого Form.bin, никаких строк подключения, никаких имён баз и хостов. Индекс должен оставаться обезличенным - чтобы его можно было коммитить в репозиторий вместе с выгрузкой.
Подключение к агенту: распаковка как предварительный шаг
Финальный этап - встроить распаковку в общий пайплайн индексации, а не дёргать вручную. Это и превращает Form.bin из «слепого пятна» в обычный артефакт.

Схема показывает логику пайплайна; конкретные имена методов зависят от реализации.
Ключевые свойства такой схемы:
-
Идемпотентность. Повторный запуск index_cf не перевыгружает формы, у которых bin_mtime == unpacked_mtime - только если .cf-файл изменился.
-
Отказоустойчивость. Если по одной форме extraction_ok=False - пайплайн не падает, индекс честно помечает её как частичную.
-
Прозрачность для агента. Со стороны code_context() это просто ещё один источник текстов. Агент не знает, что под капотом был бинарник.
Что бы я сделал иначе
Если бы собирал это заново - несколько вещей сэкономили бы мне дни.
-
Сразу проверял бы формат входа. Пофайловая выгрузка против .cf-файла - это первое, что стоит проверить, а не последнее. Теперь у меня на входе ассерт по сигнатуре.
-
extraction_ok завёл бы флагом с самого начала. Сначала я делал частичную распаковку исключением - и пайплайн валился на одной кривой форме из сотни. Флаг вместо исключения снял проблему: одна форма не должна ронять индексацию всех остальных.
-
Для .epf/.erf и EDT индекс актуальности (mtime) поставил бы с первого дня. В сценарии с .cf-файлом рассинхрона как такового нет - либо прогнал extract(), либо нет. А вот для внешних обработок и отчётов, где каждый файл меняется независимо, неделя на «странные ответы агента по устаревшим данным» - это ровно цена отложенного mtime-контроля.
-
regex по СКД - компромисс, и я это держу в голове. Он рвётся на нестандартной сериализации. Пока хватает, но честный парсер Template.bin остаётся в бэклоге. Если кто-то уже сделал - напишите, не хочу изобретать велосипед второй раз.
-
Form.obj.bsl - это не вся форма. Это я понял позже, чем следовало. Рядом лежит elem.json с полным деревом элементов - реквизиты, группы, вложенные группы, привязки. Без него агент видит обработчики, но не видит структуру. Индексировать нужно оба файла.
Чек-лист для применения
1. Поставить распаковщик: pip install v8unpack (Python, покрывает .cf, .cfe, .epf; для .erf поддержка включена в pip-релиз (PR #29 принят) или e8tools) или установить e8tools/v8unpack из системных пакетов (если Python нежелателен).
2. Взять фабрику путей из v8unpack-agent или реализовать по конвенции Form/<имя>/Form.obj.bsl.
3. Завести FormArtifact с явными флагами extraction_ok и skd_extracted.
4. Сделать forms_index (JSON) с полями по таблице выше и проверкой bin_mtime vs unpacked_mtime.
5. Подключить распаковку как предварительный шаг перед индексацией для RAG.
6. Для .erf - добавить шаг extract_skd_queries.py; убедиться, что ошибка шага не прерывает цикл.
7. Прогнать пайплайн на тестовой выгрузке и проверить, что code_context() возвращает код форм, а не только модулей.
8. Зафиксировать как инженерное правило: распаковка идёт до ревью PR, ревью - уже по текстовому слою.
Что дальше
Эта статья закрывает первый слой - распаковку Form.bin в текстовый вид.
Следующая - про elem.json: как разобрать дерево элементов формы и построить form_elements_index, чтобы агент видел не только обработчики, но и структуру.
Ссылки
-
saby-integration/v8unpack - Python, MIT, pip install v8unpack.
-
saby-integration/v8unpack PR #29 - поддержка .erf (ExternalReport); принят, включён в релиз.
-
e8tools/v8unpack - C++ gcc-порт, MPL-2.0.
-
Infactum/onec_dtools - историческая Python-реализация, на которой основан saby.
-
v8unpack-agent - Python-надстройка над v8unpack для агентных пайплайнов: фабрика путей, FormArtifact, sync_index. MIT.
Вступайте в нашу телеграмм-группу Инфостарт