Формат widget.zip
Релиз — это один ZIP-архив. Вы загружаете его в релиз, Bito проверяет архив до того, как распакует хотя бы один байт, и сохраняет только при успешной проверке. Отчёт возвращается вам сразу же — каждая ошибка называет нарушенное правило.
Структура каталогов
widget.zip
├── manifest.json обязателен, в корне архива
├── script.js обязателен, в корне архива
├── style.css необязателен
├── i18n/
│ ├── uz.json по файлу на каждый язык из манифеста
│ ├── ru.json
│ └── en.json
├── templates/ необязателен, имена любые
│ └── card.twig
└── images/ необязателен
├── logo.png
└── menu.png необязателен, иконка левого менюmanifest.json и script.js должны лежать в корне, а не внутри папки. Заархивировать каталог вместо его содержимого — самая частая причина отказа при загрузке.
manifest.json
{
"widget": { "interface_version": 2 },
"bito": {
"schema_version": 1,
"locations": {
"surfaces": ["customer_profile", "card_phone", "list:customers"],
"persistent": false
},
"network": {
"connect_origins": ["https://api.example.com", "wss://events.example.com"]
},
"pipeline_endpoint": "https://api.example.com/bito/pipeline",
"left_menu": {
"widget_page": {
"title_key": "app_title",
"icon": "images/menu.png",
"submenu": {
"deals": { "title_key": "deals" },
"reports": { "title_key": "reports" }
}
},
"saleMain": {
"submenu": { "order_sync": { "title_key": "order_sync" } }
}
}
},
"languages": ["uz", "ru", "en"],
"default_language": "uz"
}| Поле | Обязательно | Правила |
|---|---|---|
widget.interface_version | Да | Число. 2 — текущий внутристраничный рантайм. |
bito.schema_version | Да | Число. Текущее значение — 1. |
languages | Да | Массив кодов языков. Для каждого в архиве должен быть i18n/<lang>.json. |
default_language | Да | Строка. Используется, если языка пользователя нет в languages. |
bito.locations.surfaces | Нет | Массив точек монтирования. Каждый элемент — именованная точка, list:<page_name> или global. Дубликаты отклоняются. |
bito.locations.persistent | Нет | Булево. true включает постоянный, всегда смонтированный слот. Указание global в surfaces включает его тоже — считаются обе формы. |
bito.network.connect_origins | Нет | Массив точных origin. Только раскрытие — см. ниже. |
bito.pipeline_endpoint | Нет | Точный https:// URL. Туда воронка заказов отправляет действие widget_action. |
bito.left_menu | Нет | Страницы в левом меню админ-панели: собственный раздел виджета и/или страницы подменю в существующих модулях. Требует код виджета у приложения; см. левое меню. |
Всё, что не разбирается как JSON или не совпадает с этой формой, — ошибка валидации с сообщением, а не серверная ошибка.
connect_origins — раскрытие, а не ограничение
DANGER
bito.network.connect_origins показывается компании на экране согласия и никогда не применяется технически. Ничто не мешает вашему виджету — или его скомпрометированной версии — обратиться к адресу, которого нет в этом списке.
Применить его и невозможно: CSP уровня страницы действует на весь документ и не может быть ограничен одним скриптом, а ваш код работает в странице хоста. Считайте это поле декларацией, которую вы даёте компании, а не ограничением, которое Bito накладывает на вас. Не описывайте его своим пользователям как песочницу.
Форма при загрузке проверяется, чтобы на экране согласия был аккуратный список:
- только точный origin
https://илиwss://; - никаких подстановок — символ
*в строке означает немедленный отказ; - никакой «голой» схемы (
https:); - ни пути, ни query, ни фрагмента, ни завершающего слеша — строка должна совпадать сама с собой как origin.
pipeline_endpoint
Если компания настроит на этапе воронки заказов действие типа widget_action для вашего приложения, Bito возьмёт адрес из манифеста вашего согласованного релиза — компания не вводит URL вручную. Отправляется POST:
{
"event": "pipeline_action",
"db_name": "tenant_db",
"sale_order_id": "6712f0a9c4d1e87b3a0f1122",
"organization_id": "6712f0a9c4d1e87b3a0f1130",
"state_id": "6712f0a9c4d1e87b3a0f1140",
"client_id": "69c50a2d50f7b939923a469b"
}При загрузке проверяется только форма: это должен быть точный https:// URL. При отправке адрес проверяется повторно — не-HTTPS, loopback, приватные и link-local адреса отклоняются, а у запроса есть жёсткий таймаут.
Левое меню
bito.left_menu даёт установленному виджету собственные страницы в левом меню админ-панели. Он построен по образцу left_menu и колбэка initMenuPage из amoCRM. Пункты меню и заголовок страницы рисует Bito; содержимое страницы — то, что отрисует ваш виджет. Каждая такая страница монтируется в точке widget_page.
bito.left_menu заменяет bito.settings_page и bito.menu. Оба поля удалены: манифест, в котором ещё объявлено любое из них, отклоняется с сообщением, указывающим на left_menu.
Каждый ключ left_menu — это блок: widget_page или ключ модуля.
| Блок | Поля | Правила |
|---|---|---|
widget_page | title_key (обязательно), icon, submenu | Собственный раздел виджета верхнего уровня. Без submenu это одна страница; в submenu не более 10 пунктов. |
| ключ модуля | submenu (обязательно) | От 1 до 3 страниц в группе Расширения этого модуля. title_key и icon отклоняются — у модуля остаются собственные название и иконка. |
Ключ модуля должен быть одним из следующих; любой другой ключ отклоняется. Префикс — начало URL страницы.
| Ключ модуля | Префикс URL |
|---|---|
saleMain | /sale |
customerMain | /customer |
distributionMain | /distribution |
logisticsOrder | /logistics |
supplyMain | /supply |
warehouseMain | /warehouse |
financeMain | /finance |
hrMain | /hr |
integrationMain | /integration |
manufacturingMain | /manufacturing |
referenceMain | /reference |
toolsMain | /tools |
submenu — объект: ключ — код подпункта, значение — { "title_key": "..." }. Код подпункта должен соответствовать ^[a-z][a-z0-9_]{1,31}$ — строчная латинская буква, затем от 1 до 31 строчной буквы, цифры или подчёркивания. Пункты показываются в том порядке, в котором вы их объявили.
Названия
Каждый title_key обязан быть в i18n/<default_language>.json, иначе загрузка отклоняется. Названия берутся из архива при загрузке, поэтому текст меню не расходится с проверенным пакетом. При показе каждое название берётся из bundle языка пользователя; если ключа там нет, используется значение из default_language.
Иконка
icon необязательна и допустима только в widget_page. Загрузка отклоняется, если это не:
- путь к файлу архива внутри
images/с окончанием.png; - PNG-файл размером не более 16 КБ (16 384 байта);
- квадрат со стороной от 48 до 128 px;
- изображение с альфа-каналом (тип цвета PNG 4 — оттенки серого + альфа, или 6 — RGBA).
Bito использует только форму иконки. Альфа-канал PNG применяется как маска и заливается текущим цветом темы меню, поэтому иконка следует светлой и тёмной теме, наведению и активному состоянию; ваши собственные цвета не показываются никогда. Рисуйте один глиф на прозрачном фоне. Без иконки у пункта показывается стандартный глиф расширения Bito.
Код виджета и URL
{widget_code} в URL меню — код виджета приложения, то есть system_id, который вы выбираете и занимаете в Моих приложениях (см. FAQ). Если у приложения нет кода (например, у частного виджета), вместо него используется client_id: /widget/{client_id}/{subitem}.
| Страница | URL | location |
|---|---|---|
widget_page без подменю | /widget/{widget_code} | widget_page |
Пункт подменю widget_page | /widget/{widget_code}/{subitem} | widget_page |
| Пункт подменю модуля | {prefix}/widget/{widget_code}/{subitem}, например /sale/widget/acme_crm/order_sync | ключ модуля, например saleMain |
URL блока или подпункта, которого нет в манифесте, показывает сообщение «недоступно» вместо виджета. Странице модуля всегда нужен код подпункта. Какая страница открыта, виджет узнаёт из initMenuPage и bito.context.menu.
Видимость и доступ
- Меню показывает разделы
widget_pageне более чем пяти виджетов; разделы остальных виджетов не показываются. На пункты в модулях это ограничение не влияет. - Пункты меню и страницы видны всем пользователям компании; Bito не фильтрует их по ролям. Что может видеть и делать каждый пользователь, решает ваш виджет.
- RBAC Bito по-прежнему применяется к каждому вызову
bito.ajax, поэтому страница не может прочитать или изменить ничего сверх прав вошедшего пользователя. - Пункты появляются после установки приложения. Ваш код на странице запускается только после того, как компания приняла согласие на релиз, — как в любой точке монтирования.
Отличия от amoCRM
- Поле названия —
title_key(ключ i18n). - Нет
sort: порядок объявления и есть порядок показа. - Нет
is_hidden. - Иконка — PNG, который используется как форма и окрашивается цветами темы Bito, а не SVG, показанный как нарисован.
- Ключи модулей — собственные ключи Bito, перечисленные выше.
Языковые словари
Плоский JSON-объект ключ → строка. Bito выбирает на сервере один словарь при монтировании: язык пользователя, если он объявлен в манифесте, иначе default_language. Виджет получает только его — через bito.i18n().
{
"title": "История звонков",
"empty": "Пока ничего нет",
"greeting": "Здравствуйте, {{name}}"
}Словарь с невалидным JSON пишется в лог и считается пустым — виджет всё равно смонтируется, а каждый ключ вернёт сам себя.
templates/
Каждый файл из templates/ подкладывается в ответ mount и адресуется по пути после префикса templates/: templates/card.twig — это card.twig. Рендеринг — простая подстановка в двойных фигурных скобках, и ничего больше: ни циклов, ни условий, ни фильтров. Расширение .twig разрешено для привычности; это не движок Twig.
style.css
Если файл есть, он подключается к документу хоста как <link> на время работы виджета и удаляется при размонтировании. Он ничем не ограничен по области: голое правило .card { ... } перекрасит админ-панель. Добавляйте префикс каждому селектору.
script.js
AMD-модуль. См. жизненный цикл CustomWidget.
Лимиты
Все они проверяются по центральному каталогу ZIP до того, как что-либо распаковано, поэтому zip-бомба отклоняется, так и не будучи развёрнутой.
| Лимит | Значение |
|---|---|
| Сжатый архив | 10 МиБ (10 485 760 байт) |
| Суммарный несжатый размер | 50 МиБ (52 428 800 байт) |
| Один файл в несжатом виде | 5 МиБ (5 242 880 байт) |
| Количество записей | 1000 |
| Коэффициент распаковки, на запись | 100:1 |
Из-за чего запись отклоняется
| Правило | Подробности |
|---|---|
| Абсолютные пути | Ведущий / или любой абсолютный путь |
| Выход за пределы | Любой сегмент пути .. |
| Обратные слеши | \ в любом месте имени записи |
| Символические ссылки | Отклоняются безусловно, независимо от того, какую ОС заявляет архив |
| Зашифрованные записи | Записи под паролем не принимаются |
| Вложенные архивы | .zip .rar .7z .tar .gz .tgz .bz2 |
| Бит исполнения | Любой из битов режима 0o111 в Unix |
| Дубликаты путей | Одно и то же имя записи дважды |
| Неизвестное расширение | Всё, чего нет в списке ниже |
| Неверная кодировка текста | Текстовый файл, не являющийся валидным UTF-8 |
| Не изображение | PNG, JPEG и WebP проверяются по сигнатуре; расширению не верят |
Разрешённые расширения: .js .json .css .html .twig .txt .png .jpg .jpeg .webp .woff2
Типы содержимого назначаются на сервере по этому расширению, а не по тому, что заявляет архив или запрос загрузки.
Отчёт валидации
upload отвечает отчётом, и он сохраняется в релизе независимо от того, прошла проверка или нет:
{
"ok": false,
"errors": [
"Extension not allowed: vendor/lib.map",
"Missing i18n/ru.json declared in manifest.json"
],
"entries": 14,
"uncompressed_bytes": 189342
}Когда ok равно true, отчёт также содержит разобранный manifest. Отправить релиз на проверку нельзя, пока сохранённый отчёт не скажет ok.