Статья продолжает серию о подготовке обычных форм 1С для агентного пайплайна. Разобран пакетный обход выгрузки конфигурации функцией scan_forms, сборка агрегата FormScanIndex и его сериализация в JSON, обобщение обхода на все контейнеры, чьё имя оканчивается на Form. Показана интеграция с детектом дрейфа через check_drift на основе хэшей содержимого, разделение сигналов modified и structure_modified, и строковая маршрутизация запроса через FormRouter без RAG и вызовов модели.
Первые две статьи закрыли форму как единицу. Сначала - как сделать Form.bin читаемым. Потом - как вытащить дерево элементов из elem.json. К концу этой статьи у агента на руках были три артефакта одной формы: модуль, структура, привязки.
А потом я подключил его к настоящей выгрузке. И понял, что смотрел в микроскоп, когда нужна была карта.
Одна форма - это хорошо для демонстрации. Но реальная типовая конфигурация 1С содержит сотни форм, распределённых по десяткам типов объектов. Агент не может «распаковать нужную форму»: он сначала должен узнать, что эта форма вообще существует, где она лежит и как называется её контейнер. Ему нужен реестр. Не разбор одной формы, а список всех - с адресами, чтобы было куда идти за деталями.
Эта статья - про переход от единичной формы к пакетному реестру. Про то, как я собрал первый агрегат scan_forms, как он сломался о реальную раскладку выгрузки, и почему я развёл два похожих индекса по разным классам, чтобы самому в них не запутаться.
Коротко: что делать
· Собирать реестр форм пакетно через scan_forms(cf_export_root), а не распаковывать формы по одной: агенту нужна карта всей конфигурации, а не отдельная форма.
· Обходить все контейнеры, чьё имя оканчивается на Form, а не только буквальный контейнер Form: на реальной раскладке форм в DocumentForm/CatalogForm в разы больше.
· Держать в голове два разных класса: FormsIndex - карточка распаковки одной формы, FormScanIndex - полный реестр всех форм. Это не два представления одного, это разные сущности.
· Детект дрейфа check_drift считать формы удалёнными по факту исчезновения .obj.bsl с диска, а не по флагу извлечения.
· Строить baseline дрейфа на содержимом (bsl_sha256), а не на времени изменения файла: mtime не переживает полную перераспаковку и даёт ложные срабатывания.
· Разделять два независимых сигнала: modified (изменился код формы) и structure_modified (изменилось нормализованное дерево элементов, elem_sha256).
· Маршрутизировать запрос агента к форме строковым FormRouter без вызова модели: приоритет form_name над object_name над object_type.

Рис. 1. Пайплайн реестра форм.
Зачем реестр поверх распаковки
Распаковка отвечает на вопрос «что внутри этой формы». Реестр отвечает на другой вопрос - «какие формы вообще есть и где они». Для агента это принципиально разные слои.
Пока форма одна, реестр не нужен: путь известен, распаковал, прочитал. Но агент, который помогает по конфигурации, начинает не с известного пути. Он начинает с запроса вроде «покажи форму списка номенклатуры». Чтобы этот запрос превратить в путь, нужен предварительный проход по всей выгрузке: собрать, что где лежит, и сложить в один компактный агрегат.
Именно поэтому scan_forms не распаковывает формы. Он их только находит и переписывает адреса. Тяжёлый разбор elem.json и Form.obj.bsl остаётся на потом, точечно, когда агент уже знает, к какой форме идти.
Реестр и распаковка - разные слои. Реестр отвечает «где форма», распаковка - «что внутри». Смешивать их в одной функции значит платить полным разбором за каждый обзорный запрос
Проблема: предыдущая статья остановилась на одной форме
К концу предыдущей статьи пайплайн умел обрабатывать конкретную форму по известному пути. Я честно считал, что осталось только «прогнать это в цикле по всем формам». Наивно.
Первая же попытка запустить обход показала, что я не знаю двух вещей. Во-первых, где в выгрузке лежат формы - реальная конфигурация 1С использует имена контейнеров по типу объекта (DocumentForm, CatalogForm), а не единственный Form. v8unpack воспроизводит эту структуру точно, но без интеграционных тестов на живой конфигурации это стандартная ловушка. Во-вторых, как отличить «форму, которую агент уже видел» от «формы, которая изменилась с прошлого прохода».
Обе проблемы решались не циклом. Первая - обобщением обхода. Вторая - отдельным механизмом сравнения check_drift. Про них дальше.
scan_forms.py: обход выгрузки
Функция намеренно принимает только два параметра и ничего лишнего не возвращает - это снижает связность. На вход - корень выгрузки, на выход - агрегат.
from pathlib import Path
from v8unpack_agent.scan_forms import scan_forms, FormScanIndex, FormEntry
index: FormScanIndex = scan_forms(cf_export_root=Path("cf_export/"))
# index.forms: list[FormEntry]
# index.total: int
# index.scanned_at: str # метка времени в ISO-8601
# index.scan_warnings: list[str] # предупреждения по формам, которые не удалось прочитать
index.save(Path("forms_scan_index.json"))
Обход идёт в режиме best-effort. Ошибка на одной форме не роняет весь проход: проблема пишется в scan_warnings, обход продолжается. На сотнях форм гарантированно найдётся пара директорий с сюрпризом, и одна битая форма не должна обнулять реестр целиком.
Пакетный обход всегда best-effort. На реальном объёме одна нечитаемая форма - норма, а не авария. Пишите её в предупреждения и идите дальше.
Запуск из CLI
Обход доступен не только как функция, но и как модуль командной строки. Это удобно для разовой сборки реестра или для встраивания в скрипт CI.
python -m v8unpack_agent.scan_forms cf_export/ --mode config --save
вывод
Найдено форм: 14
Реестр сохранён: forms_scan_index.json
Позиционный аргумент - корень выгрузки; --mode {config,external} - режим обхода, по умолчанию config; --save - сохранить реестр в forms_scan_index.json. Запуск через python -m намеренно не тянет тяжёлые зависимости пакета: сам модуль scan_forms подгружается лениво через __getattr__.
FormScanIndex: структура, поля и два разных индекса
В пакете есть два индекса с похожими именами и совершенно разным смыслом.
|
Класс |
Что это |
Уровень |
Ключевые поля |
|
FormsIndex |
Карточка распаковки одной формы (статьи 02–03) |
Одна форма |
Статус извлечения, метки времени распаковки |
|
FormScanIndex |
Полный реестр всех форм конфигурации (эта статья) |
Вся выгрузка |
forms, total, scanned_at, scan_warnings |
FormsIndex отвечает на вопрос «в каком состоянии распакована вот эта форма». FormScanIndex отвечает на вопрос «какие формы вообще есть». Я намеренно держу их раздельно и называю файл реестра forms_scan_index.json.
@dataclass
class FormEntry:
object_type: str # "Catalog", "Document", ...
object_name: str # "Demo", "SalesOrder", ...
container_name: str # "CatalogForm", "DocumentForm", "Form", ...
form_name: str # "ФормаСписка", "ФормаЭлемента", ...
form_path: str # относительный путь к директории формы
bsl_path: str # путь к <ContainerName>.obj.bsl
json_path: str # путь к <ContainerName>.json
bsl_sha256: str | None # хэш содержимого .obj.bsl (baseline дрейфа кода)
elem_sha256: str | None # хэш нормализованного дерева элементов формы
bsl_mtime: float # вспом. метка времени изменения
warnings: list[str] # предупреждения по конкретной форме
{
"object_type": "Catalog",
"object_name": "Demo",
"container_name": "CatalogForm",
"form_name": "ФормаСписка",
"form_path": "cf_export/Catalog/Demo/CatalogForm/ФормаСписка",
"bsl_path": "cf_export/Catalog/Demo/CatalogForm/ФормаСписка/CatalogForm.obj.bsl",
"json_path": "cf_export/Catalog/Demo/CatalogForm/ФормаСписка/CatalogForm.json",
"bsl_sha256": "3b1f...e9a0",
"elem_sha256": "7c42...01d5",
"bsl_mtime": 1718450000.0,
"warnings": []
}
Если в пакете два похоже названных индекса - разведите их по разным классам и разным именам файлов на диске.
Реальный layout: обобщение на контейнеры *Form
Когда я писал scan_forms на демо-конфигурации, раскладка выглядела так: cf_export/<Тип>/<Объект>/Form/<ИмяФормы>/. Контейнер называется Form, внутри - директории форм. Первый обход искал строго контейнер с именем Form.
Потом я прогнал его на конфигурации побольше - и получил реестр, в котором форм было подозрительно мало. Имя контейнера зависит от типа объекта конфигурации: DocumentForm, CatalogForm, ReportForm и так далее. Мой обход по буквальному Form их просто не видел.
Распределение по контейнерам после прогона scan_forms на тестовой выгрузке:
|
Контейнер |
Кол-во форм |
|
DocumentForm |
693 |
|
CatalogForm |
474 |
|
Form |
417 |
|
ReportForm |
166 |
|
CommonForm |
162 |
|
InformationRegisterForm |
148 |
|
ExchangePlanForm |
34 |
|
ChartOfCharacteristicTypeForm |
19 |
|
AccumulationRegisterForm |
17 |
|
BusinessProcessForm |
10 |
|
DocumentJournalForm |
9 |
|
TaskForm |
7 |
|
EnumForm |
4 |
|
FilterCriterionForm |
4 |
Итого: 2164 формы, из которых буквальный контейнер Form - меньше пятой части. Лечение - обобщить условие: вместо «имя равно Form» стало «имя оканчивается на Form». Заодно в FormEntry появилось поле container_name: без него две одноимённые формы в разных контейнерах сливались бы в одну запись.
Синтетическая раскладка на демо почти всегда проще реальной. Прогоните обход на конфигурации побольше и сверьте число найденных форм с ожиданием. Тихая потеря четырёх пятых данных не даёт ни ошибки, ни лога.
Интеграция с детектом дрейфа
Реестр без детекта изменений живёт ровно до первой правки конфигурации. Поэтому рядом со scan_forms есть check_drift.
from pathlib import Path
from v8unpack_agent.drift_checker import check_drift, DriftReport
report: DriftReport = check_drift(
cf_export_root=Path("cf_export/"),
index_path=Path("forms_scan_index.json"),
)
# report.has_drift: bool
# report.added: list[str] # формы на диске, которых нет в реестре
# report.removed: list[str] # формы из реестра, которых нет на диске
# report.modified: list[str] # изменился код формы (.obj.bsl)
# report.structure_modified: list[str] # изменилось дерево элементов формы
# report.stale_extractions: list[str] # формы, чей .obj.bsl исчез с диска
# report.checked_at: str

Рис. 2. Детект дрейфа: сохранённый реестр и текущая выгрузка сравниваются в DriftReport
stale_extractions - это не «форма с флагом неуспешного извлечения». В реестре такого флага нет. stale_extractions - формы, которые были в реестре, но их файл .obj.bsl исчез с диска. Семантика именно «протухшая ссылка на извлечение», а не «извлечение не удалось».
Детект по mtime разваливается при полной перераспаковке: файловая система проставляет всем .obj.bsl новые времена изменения, хотя содержимое кода не поменялось ни на байт. Лечение - перевести baseline с файлового атрибута на содержимое: content-hash bsl_sha256.
structure_modified - отдельный и независимый от modified сигнал. modified отвечает «изменился ли код формы», structure_modified - «изменилась ли разметка, её дерево элементов». Хэшируется нормализованное дерево элементов: из него оставлены только структурные поля, вся косметика отброшена.
Сводка сигналов по сценариям:
|
Действие |
modified |
structure_modified |
|
Правка кода (BSL) |
да |
нет |
|
Добавление/удаление элемента, код не тронут |
нет |
да |
|
Правка кода и разметки |
да |
да |
|
Косметика (координаты/цвета/шрифты) |
нет |
нет |
|
Повторная полная перераспаковка неизменённого .cf |
нет |
нет |
stale_extractions - исчезнувший с диска .obj.bsl, а не «форма не извлеклась». Первая означает битую ссылку в индексе, вторая - неполноту контекста.
Baseline дрейфа стройте на содержимом, а не на mtime. Content-hash (bsl_sha256 для кода, elem_sha256 для нормализованной разметки) - устойчивый baseline.
Агент работает с реестром: FormRouter
Реестр собран. Теперь запрос агента надо превратить в конкретную форму. Этим занимается FormRouter.
from pathlib import Path
from v8unpack_agent.form_router import FormRouter, RouteResult
router = FormRouter(index_path=Path("forms_scan_index.json"))
result: RouteResult = router.route(query="ФормаСписка Catalog")
# result.matched: list[FormEntry]
# result.confidence: float # 0.0-1.0
# result.warnings: list[str]
Маршрутизация версии 1 - чисто строковая. Никаких вызовов модели, никакого RAG. Просто сравнение строк запроса с полями записей, с приоритетом: точное совпадение по form_name весит больше, чем совпадение по object_name, а оно - больше, чем подстрочное совпадение по object_type.
reindex(changed_forms) обновляет записи реестра точечно, не пересобирая всё. Два ключевых требования к инкрементальному обновлению: (1) reindex обязан сохранять baseline дрейфа при записи; (2) ключ формы обязан включать container_name - иначе две одноимённые формы в разных контейнерах перезапишут друг друга. Ключ везде собирается одним хелпером из четырёх компонент: (object_type, object_name, container_name, form_name).
Строковый роутер без модели - нормальная первая версия. Инкрементальное обновление обязано сохранять baseline и составной ключ из четырёх компонент.
External-режим: внешние обработки и отчёты
Формы живут не только внутри выгрузки конфигурации. Внешние обработки и отчёты (.epf/.erf) при распаковке дают свою раскладку, и для них у scan_forms есть отдельный режим.
from pathlib import Path
from v8unpack_agent.scan_forms import scan_forms, FormScanIndex
index: FormScanIndex = scan_forms(
Path("external_export/"),
mode="external",
)
Раскладка внешнего объекта: External/<ИмяОбъекта>/Form/<ИмяФормы>/ и External/<ИмяОбъекта>/ReportForm/<ИмяФормы>/. Режим external изолирован от config по object_type: внешним объектам присваивается ExternalDataProcessor или ExternalReport. Эти типы не пересекаются с конфигурационными.
Внешние обработки и отчёты - не частный случай одного Form. Изолируйте их по object_type, храните реальный container_name и ищите артефакты по имени контейнера.
Что бы я сделал иначе
· Заложил бы обобщение на *Form-контейнеры с самого начала. Ставка на буквальный Form стоила реестра, в котором молча терялось четыре пятых форм.
· Сразу развёл бы FormsIndex и FormScanIndex по разным именам файлов - совпадение имён рано или поздно приводит к молчаливой перезаписи.
· Строил бы baseline дрейфа на содержимом с самого начала. Первая версия опиралась на mtime и разваливалась на первой же полной перераспаковке.
· Провёл бы baseline дрейфа через сохранение в роутере с самого начала - ранняя версия reindex его теряла.
· Сделал бы ключ формы составным везде одинаково сразу. Был период, когда check_drift уже включал container_name в ключ, а reindex роутера - ещё нет.
· Не завышал бы confidence роутера искусственно. Формула для совпадения по object_name фактически всегда даёт одно и то же значение - это вводит в заблуждение.
Чек-лист для применения
1. Собирать реестр через scan_forms(cf_export_root) пакетно, не распаковывая формы по одной.
2. Обходить все контейнеры, чьё имя оканчивается на Form, а не только буквальный Form: иначе потеряете большую часть форм.
3. Хранить в записи container_name, чтобы одноимённые формы в разных контейнерах не сливались.
4. Сохранять реестр в forms_scan_index.json, не в forms_index.json: второе имя занято карточкой распаковки из предыдущих статей.
5. Проверять именно пару <ContainerName>.obj.bsl (обязателен) и <ContainerName>.json (при отсутствии - предупреждение).
6. Трактовать stale_extractions как исчезновение .obj.bsl с диска, а не как флаг неуспешного извлечения.
7. Строить baseline дрейфа кода на content-hash bsl_sha256, а не на bsl_mtime: mtime не переживает полную перераспаковку.
8. Различать modified (изменился код формы) и structure_modified (изменилось нормализованное дерево элементов) как два независимых сигнала.
9. Маршрутизировать запрос строковым FormRouter с явным приоритетом form_name > object_name > object_type, без вызова модели.
10. При инкрементальном reindex проверять, что хэшевый baseline и составной ключ формы сохраняются.
Что дальше
Реестр даёт агенту карту всех форм, а строковый роутер — быстрый путь к форме по известным именам. Следующий шаг — собрать из модуля, дерева элементов и метаданных компактный контекст формы и проиндексировать его для смыслового поиска. FormRouter при этом останется первым, дешёвым уровнем маршрутизации, а RAG (Retrieval-Augmented Generation — дополнение запроса контекстом из поискового индекса) будет подключаться, когда точное имя формы неизвестно.
Ссылки
· Обычные формы 1С в агентном пайплайне: пошаговая распаковка - //infostart.ru/1c/articles/2721726/ - Первая статья про распаковку Form.bin
· СКД и дерево элементов обычной формы 1С: два некритичных шага в агентном пайплайне - //infostart.ru/1c/articles/2726561/ - Вторая статья про СКД и разбор elem.json
· v8unpack-agent - https://github.com/MRDK80/v8unpack-agent - Python-надстройка над v8unpack: scan_forms, drift_checker, form_router, CLI. MIT
· saby-integration/v8unpack - https://github.com/saby-integration/v8unpack - Python, MIT, pip install v8unpack
Вступайте в нашу телеграмм-группу Инфостарт