Статья продолжает разбор подготовки обычных форм 1С для агентного пайплайна и описывает два некритичных шага одного релиза. Первый: пакетное извлечение запросов СКД (Система компоновки данных - встроенный в 1С механизм построения отчётов) из Template.bin внешних отчетов через extract_all_skd_queries, с записью отдельного skd_queries.json по каждому отчету и обработкой ошибок в режиме best-effort. Второй, основной: разбор сериализованного дерева элементов формы из elem.json и построение компактного form_elements_index, который сжимает исходный файл более чем в 10 раз, отбрасывая визуальные свойства и оставляя структуру. Подробно разобраны два прохода парсера, восстановление иерархии из путей-ключей, коллизии одноименных страниц через parent_path и граница достоверности: почему вложенность групп намеренно не реконструируется. Все примеры синтетические, на demo-конфигурации, код - Python.
Предыдущая статья закрыла первый вопрос: как сделать Form.bin читаемым. Казалось, дальше всё просто - агент видит Form.obj.bsl, живёт и радуется.
Не тут-то было!
Когда я полез подключать агента к отчётам - и вскрылся первый незакрытый хвост: пообъектный extract_skd_queries при передаче корня с несколькими .erf просто брал первый найденный Template.bin и уходил. Остальные отчёты - как будто их нет. Молча, без ошибки. Это я поймал только когда начал проверять выдачу агента по конкретным отчётам и удивился, почему у половины нет СКД.
Второй хвост оказался крупнее. Form.obj.bsl - это только обработчики. Рядом в директории формы лежит CatalogForm.elem.json - сериализованное дерево элементов: кнопки, поля, группы, привязки к реквизитам. Без него агент видит процедуры, но не видит структуру: на какой вкладке кнопка, к какому реквизиту привязано поле, что вообще на этой форме есть. Слепое пятно номер два.
Эта статья закрывает обе темы. Сначала быстро разберём доработку СКД - это прямое продолжение прошлой статьи. Потом основная часть: elem.json и form_elements_index.
Коротко: что делать
-
Для пакета отчётов вызывать extract_all_skd_queries, а не пообъектную функцию: один корень над несколькими Template.bin иначе даст только первый отчёт.
-
Проверить, что skd_queries.json пишется внутрь директории каждого отчёта, а не на уровень выше.
-
Трактовать СКД-шаг как best-effort: skd_extracted=False при extraction_ok=True - это сигнал о неполноте контекста, а не авария пайплайна.
-
Строить form_elements_index из elem.json, а не подавать elem.json в RAG (Retrieval-Augmented Generation - метод дополнения LLM контекстом из индекса) целиком: визуальные свойства раздувают файл в десятки раз и агенту не нужны.
-
Использовать path как канонический адрес элемента, parent_path - для разрешения коллизий одноименных страниц.
-
Не доверять реконструкции вложенности групп: смотреть warnings в индексе.
Доработка СКД: от скрипта к модулю пакета
Template.bin внутри .erf содержит запросы СКД - для агента это контекст выборки данных: какие источники участвуют в отчёте, что выбирается, какие условия. Без этого слоя агент видит только BSL (Built-in Script Language - встроенный язык 1С)-обработчики отчёта, но не понимает, что он вообще считает.
Пообъектный extract_skd_queries я написал первым. Работает нормально, когда передаёшь ему корень одного отчёта. Но как только выгрузка содержит несколько .erf под одним корнем - он брал первый найденный Template.bin и останавливался. Я потратил немного времени, пока не дошло, что проблема именно здесь: в памяти возвращался один SkdResult, и это не выглядело как ошибка.
Пакетный режим решает это в лоб:
| Функция | Режим | Поведение |
| extract_skd_queries(unpacked_root) | пообъектный | один отчёт → SkdResult; берёт первый Template.bin |
| extract_all_skd_queries(unpacked_root) | пакетный | обходит все Template.bin → SkdBatchResult |
from pathlib import Path
from v8unpack_agent import extract_all_skd_queries, SkdBatchResult
batch: SkdBatchResult = extract_all_skd_queries(unpacked_root=Path("src/Report"))
# batch.skd_extracted: bool - True если хотя бы один отчёт извлёкся
# batch.results: list[SkdResult]
# batch.warnings: list[str] - предупреждения по неудачным отчётам
Ошибка одного отчёта не прерывает обработку остальных - принцип тот же, что и в extraction_ok у форм.
Отдельная история с путём записи файла. В исходной реализации skd_queries.json писался на уровень выше корня отчёта. При пакетном обходе это означало: каждый следующий отчёт тихо перезаписывал файл предыдущего. В памяти batch.results собирал все SkdResult корректно, а на диске оставался только последний. Я это поймал только когда стал проверять файлы руками - в коде всё выглядело правильно. После небольшой правки каждый файл теперь пишется внутрь директории каждого отчёта.
Практическое правило: skd_extracted=False при extraction_ok=True - это не авария. Это сигнал агенту: BSL-слой доступен, но контекст отчёта неполный. То же самое справедливо для elem_index_ok у форм.
Дерево элементов обычной формы
Вот тут я потратил больше всего времени. Казалось бы - elem.json это просто JSON. Открыл, прочитал. Всё хорошо, но нет.
Что такое elem.json
После распаковки Form.bin через v8unpack в директории формы появляется несколько файлов. Form.obj.bsl - модуль формы: процедуры и обработчики. Его агент читает нормально.
CatalogForm.elem.json (имя варьируется по типу объекта) - сериализованное дерево элементов формы. Здесь хранится:
-
иерархия элементов: Group, Panel, Button, Field, Label, CommandPanel;
-
вложенность: страницы Panel, Group внутри страницы;
-
имена элементов - программные идентификаторы, которые используются в BSL через ЭтотОбъект.Элементы.<Имя>;
-
привязки реквизитов: Field → ссылка на реквизит объекта;
-
описание обработчиков: имя элемента → имя процедуры в Form.obj.bsl;
-
визуальные свойства: координаты, размеры, цвета.
Что агент видит без elem.json
Смотришь на Form.obj.bsl после распаковки - там что-то вроде:
// Form.obj.bsl
Процедура КнопкаВоВложеннойГруппеНажатие(Команда)
// ... тело обработчика ...
КонецПроцедуры
Код есть. Имя процедуры есть. Но агент не знает, что КнопкаВоВложеннойГруппе - это именно кнопка, где она расположена в дереве, на какой странице, внутри какой панели. Не знает, какие реквизиты вообще отображаются на форме. Он видит процедуры без контекста.
Спросить агента «что делает эта форма» - ответит что-то вроде «содержит обработчик нажатия кнопки». Технически верно. Практически бесполезно.
Почему elem.json - не готовое дерево
Я рассчитывал просто распарсить JSON и получить дерево. Не вышло.
elem.json - это сериализованный формат платформы. Иерархия элементов там не кодируется структурой JSON-объектов - она кодируется в путях-ключах секции data. Ключ вида Страница1/ПанельВерхняя/Страница1/ПанельВложенная/Страница11/Код - это и есть адрес элемента Код в дереве формы. Чтобы восстановить иерархию, нужно разобрать пути.
Плюс несколько неочевидных вещей:
-
страницы Panel могут быть одноименными на разных уровнях - без parent_path они неотличимы;
-
пустые страницы без элементов-потомков хранятся в ключах -pages-, а не как отдельные объекты;
-
вложенность групп в путях-ключах не отражается вообще - она в бинарном слое;
-
реквизиты формы из секции props - не визуальные элементы, у них нет места в дереве;
-
GUID реквизитов резолвятся через Catalog.json / Catalog-3.json, то есть через отдельный файл метаданных.
Это не «просто прочитать JSON». Это «восстановить дерево из путей, зная, что формат сериализует иерархию нестандартно».
Два прохода парсера
Алгоритм работает в два прохода.
Проход 1 - секция data. Ключи разбираются как пути: из каждого пути извлекаются name (последний сегмент), parent (предпоследний), parent_path (всё до последнего сегмента), path (полный ключ). Ключи -pages- дают пустые страницы, которые иначе потеряются. Типы элементов берутся из секции tree по имени.
Проход 2 - секция props. Реквизиты формы добавляются отдельной группой с source: "props", без path и parent - они не входят в визуальное дерево.
Если секция data отсутствует - парсер переключается на резервный разбор секции tree и помечает результат предупреждением.
Два кейса из demo-конфигурации
Я прогнал парсер на двух справочниках: «Номенклатура» с вложенными панелями и «Склады» с вложенными группами.
Кейс 1: вложенные Panel и страницы
Форма с ПанельВерхняя на корневой странице и ПанельВложенная внутри первой страницы внешней панели. Дерево из прогона:

Panel "ПанельВерхняя"
Page "Страница1"
Panel "ПанельВложенная"
Page "Страница11"
Field "Код", Field "Наименование", Field "Родитель", Field "Артикул"
Label "НадписьКод", ...
Page "Страница12" # пустая - из ключей -pages-
Page "Страница13" # пустая - из ключей -pages-
Page "Страница2"
Panel "ПанельВложенная2"
Page "Страница1" # одноимённый узел - коллизия!
Label "Надпись1"
В form_elements_index элемент Код выглядит так:
{
"name": "Код",
"type": "Field",
"parent": "Страница11",
"parent_path": "Страница1/ПанельВерхняя/Страница1/ПанельВложенная/Страница11",
"path": "Страница1/ПанельВерхняя/Страница1/ПанельВложенная/Страница11/Код",
"page": "Страница11",
"source": "data"
}
Страница1 в этой форме встречается на трёх уровнях. Без parent_path они неотличимы по имени:
path: "Страница1"
path: "Страница1/ПанельВерхняя/Страница1"
path: "Страница1/ПанельВерхняя/Страница2/ПанельВложенная2/Страница1"
path разрешает коллизию однозначно. Это, кстати, не экзотика - в реальных конфигурациях одноимённые страницы на разных уровнях встречаются регулярно.
Кейс 2: Group внутри страницы - и первый честный warning
Форма «Склады» с вложенными группами:

data-ключи:
Страница1/РамкаГруппыОбщая
Страница1/РамкаГруппыСлужебная # вложенная группа, но ключ плоский
Страница1/НадписьКод
Страница1/Код
Страница1/НадписьНаименование
Страница1/Наименование
Все элементы получают parent = "Страница1". Вложенность РамкаГруппыСлужебная внутри РамкаГруппыОбщая - в путях-ключах data не отражена. Она есть в raw/info-векторах каждого элемента: позиционные списки дочерних числовых id. Но raw - это недокументированный внутренний формат платформы 1С. Я посмотрел на эти векторы, подумал - и решил не трогать.
Не потому что лень, просто декодирование недокументированного формата - это и нестабильная зависимость и нарушение лицензионного соглашения 1С. Платформа обновится, формат сдвинется, парсер начнёт врать. Честный warning в индексе лучше нестабильной догадки.
Практическое правило: если агент спрашивает о вложенности групп - смотреть warnings. Если warnings содержит предупреждение о группах, иерархию групп использовать нельзя - только плоский список элементов страницы.
Граница достоверности
| Узел формы | Источник иерархии | Результат | Статус |
| Панели, страницы, включая пустые | пути-ключи секции data | полная цепочка, parent_path снимает коллизии | достоверно |
| Группы и их вложенность | только raw/info-векторы | вложенность не реконструируется, parent указывает на страницу | best-effort + warning |
Это честная граница между тем, что можно получить из публичных артефактов распаковки, и тем, что требует декодирования недокументированного формата. Я рад, что обозначил её явно, а не «дореконструировал» и получил парсер, который ломается на следующем релизе платформы.
Сколько весит elem.json и почему это важно
На двух формах цифры такие:
| Форма | elem.json | form_elements_index.json | Сжатие | Элементов |
| Номенклатура (панели) | ~124 КБ | ~10 КБ | ~12x | 24 |
| Склады (группы) | ~55 КБ | ~3 КБ | ~18x | 10 |
Большую часть исходного файла занимают координаты, цвета, шрифты и GUID метаданных. Агенту это не нужно - он не рисует интерфейс.
Подавать elem.json в RAG целиком нецелесообразно: 124 КБ мусора съедают контекстное окно и ничего полезного не дают.
Что оставляет form_elements_index
| Что нужно | Зачем агенту |
| Имя элемента + тип | понять, что есть на форме |
| path | канонический адрес элемента внутри формы |
| parent, parent_path | понять родителя, сгруппировать дочерние элементы |
| Привязка реквизита для Field | к каким данным привязано поле |
| Имя обработчика для Button/Field | связать элемент с кодом в Form.obj.bsl |
| page | на какой вкладке находится элемент |
| source | отличить элемент формы (data) от реквизита (props) |
Формат индекса:
{
"form": "Форма",
"elements": [
{
"name": "Страница1",
"type": "Page",
"parent": null,
"parent_path": null,
"path": "Страница1",
"page": null,
"source": "data"
},
{
"name": "КнопкаНаСтранице11",
"type": "Button",
"parent": "Страница11",
"parent_path": "Страница1/ПанельВерхняя/Страница1/ПанельВложенная/Страница11",
"path": "Страница1/ПанельВерхняя/Страница1/ПанельВложенная/Страница11/КнопкаНаСтранице11",
"page": "Страница11",
"source": "data",
"handler": "КнопкаНаСтранице11Нажатие"
},
{
"name": "ОбработкаОбъект",
"type": "Unknown",
"parent": null,
"parent_path": null,
"path": null,
"page": null,
"source": "props"
}
]
}
Это третий артефакт формы после Form.obj.bsl и Form.json.
Что бы я сделал иначе
-
Сразу проверял бы наличие нескольких Template.bin под одним корнем. Пообъектный режим брал только первый - всплыло не сразу, потому что в памяти всё выглядело корректно. Пакетный режим стоило заложить с самого начала, не откладывать.
-
Путь записи skd_queries.json зафиксировал бы относительно корня отчёта сразу. Запись на уровень выше при пакетном обходе тихо перезаписывала результат предыдущего - обнаружил только при ручной проверке файлов. Это классическая ошибка, которую сложно заметить в логах.
-
Границу достоверности по группам обозначил бы явно с первого дня. Соблазн «дореконструировать» вложенность из raw велик - выглядит как простая задача. Но это декодирование недокументированного формата платформы. Честный warning лучше нестабильной догадки, которая ломается при обновлении.
-
Разделение data и props через поле source ввёл бы сразу. Без него реквизиты формы и визуальные элементы смешивались в одном списке, и агент не отличал одно от другого. Пришлось переделывать индекс.
Чек-лист для применения
-
Для пакета отчётов вызывать extract_all_skd_queries, а не пообъектную функцию: один корень над несколькими Template.bin иначе даст только первый отчёт.
-
Проверить, что skd_queries.json пишется внутрь директории каждого отчёта, а не на уровень выше.
-
Трактовать СКД-шаг как best-effort: skd_extracted=False при extraction_ok=True - это сигнал о неполноте, а не авария.
-
Строить form_elements_index из elem.json, а не подавать elem.json в RAG целиком: визуальные свойства раздувают файл в десятки раз и забивают контекстное окно.
-
Использовать path как канонический адрес элемента; parent_path - для разрешения коллизий одноимённых страниц.
-
Не доверять реконструкции вложенности групп: при отсутствии иерархии групп проверять warnings в индексе.
-
Отличать элементы формы (source: "data") от реквизитов формы (source: "props") при индексации.
Что дальше
Два артефакта формы - Form.obj.bsl и form_elements_index - дают агенту и код, и структуру. Следующий шаг: связать имена обработчиков из индекса с процедурами в модуле и резолвить привязки реквизитов через Catalog.json.
Ссылки
-
Обычные формы 1С в агентном пайплайне: пошаговая распаковка - предыдущая статья серии.
-
saby-integration/v8unpack - Python, MIT, pip install v8unpack.
-
v8unpack-agent - Python-надстройка над v8unpack для агентных пайплайнов: фабрика путей, FormArtifact, извлечение СКД, парсер elem.json. MIT.
Вступайте в нашу телеграмм-группу Инфостарт