Лимиты, безопасность и частые ошибки¶
Как устроена песочница¶
- WebAssembly. Плагин выполняется в движке QuickJS, скомпилированном в WebAssembly,
внутри процесса панели. У него нет файловой системы, процессов, переменных окружения и
сети. Есть только то, что даёт
panel.*. - Сеть — только
panel.http.fetchи только к хостам изnet. Собственные адреса панели, приватные и локальные недоступны при любом DNS, в том числе после редиректа. - API панели — только с правами, которые одобрил оператор. Ключи, админов, обновления и другие плагины трогать нельзя никогда.
- Данные — своя база SQLite с квотой. Базы панели и других плагинов недоступны.
- Где работает. Только на главном сервере, не на нодах. База плагина входит в бэкапы панели.
- Что видит оператор. До включения — права, хосты, точки и код (карандаш на карточке открывает его в редакторе).
Действия плагина — в журнале панели от имени
plugin:<id>.
Лимиты¶
| Лимит | Что будет сверх него | |
|---|---|---|
| Память JS | memory_mb, 32 МБ (до 128) |
вызов бросает InternalError: out of memory, VM пересоздаётся |
| Время вызова | см. таблицу в точках расширения: 0,3 с – 60 с | InternalError: interrupted |
| База | db_quota_mb, 100 МБ (до 1024) |
запись отклоняется; плагин останавливается, если база остаётся заполненной на 90% |
| Запрос к базе | 10 000 строк, 4 МБ ответа; одно значение до 8 МБ, текст запроса до 1 МБ | ошибка — используйте LIMIT/OFFSET |
kv |
значение 64 КБ, ключ 512 байт, list до 1000 |
ошибка |
panel.api |
запрос 1 МБ, ответ 4 МБ | ошибка — забирайте страницами или в blob |
panel.http.fetch |
тело 1 МБ, ответ 4 МБ, 30 с, 5 редиректов | ошибка — большие данные через blob |
| blob | 256 МБ каждый, 16 и 512 МБ на вызов | ошибка |
onHttp |
тело 1 МБ в обе стороны | ошибка |
| Пакет | zip 5 МБ, распакованный 12 МБ, main.js 2 МБ, 200 файлов |
установка отклоняется |
| Лог | 1000 строк, строка до 4 КБ | старые строки вытесняются |
QuickJS в WebAssembly примерно в 20–30 раз медленнее нативного кода. Для интеграций этого
хватает с запасом, но тяжёлые вычисления (большие циклы, криптография на JS) в плагин не
кладите — для хэшей и подписей есть panel.crypto.
Когда панель останавливает плагин¶
Плагин переходит в статус «остановлен» (paused), а админы получают уведомление в админ-бот.
Сбои. После 10 неудачных вызовов подряд (исключение или таймаут) панель останавливает
плагин и сама пробует запустить его снова: через 5 минут, через 15, затем каждый час. События,
которые уже ждут плагин, сохраняются. После повторного запуска плагин снова останавливают 3
сбоя подряд. Плагин, который не запустился при старте панели, перезапускается так же. Про
сбои админам пишут не чаще раза в 6 часов. Сбои onHttp и webhook оплаты пишутся в лог, но
не считаются.
Квота и зацикливание. Такая остановка ждёт оператора, а события, которые ждали плагин, отбрасываются:
- база переросла квоту — запись отклонена, и после вызова база всё ещё заполнена на 90%;
- зацикливание — три минуты подряд больше 300 событий в минуту и больше 100 изменений в
минуту от самого плагина через
panel.api: плагин меняет пользователя в ответ на событие, которое это изменение снова вызывает.
Остановленный плагин не получает вызовов, а события, случившиеся за это время, до него не доходят. На карточке видно, почему он остановлен и когда панель запустит его снова. Оператор нажимает Запустить снова, когда причина устранена. Заказы его способа оплаты ждут: панель не отменяет их по возрасту, пока плагин установлен.
Упавшие subBlocks, bot.menu или transformSubscription не зовут 30 с; плагин при этом
работает.
Частые ошибки¶
| Сообщение | Причина и что делать |
|---|---|
InternalError: interrupted |
вызов не уложился во время. Вынесите долгую работу в cron, делите её на части и сохраняйте прогресс в kv |
InternalError: out of memory |
не хватило memory_mb. Не держите большие массивы целиком: читайте базу страницами, большие ответы берите в blob, поднимите memory_mb |
fetch: X is not in the plugin's net allowlist |
хоста нет в net манифеста, или порт не 80/443 у записи без порта. Добавьте его, при необходимости с :порт (это новое согласие оператора) |
plugin fetch: … is a private or local address |
хост разрешается в приватный, локальный или собственный адрес панели — к таким плагин не ходит |
fetch: redirect to …, not in the allowlist |
сервис перенаправил на хост не из net |
fetch: body over 1024 KB — send a blob |
большое тело отправляйте blob-ом |
fetch: answer over 4 MB |
получайте ответ в blob: {blob: true} |
panel.api: path "…" is not a /v1 route |
путь должен начинаться с /v1/ |
panel.api: only GET inside a decision hook |
в решениях (beforeSignup, quotePrice, …), bot.menu и transformSubscription можно только читать |
plugin: a call into itself (through the panel) is refused |
запрос panel.api привёл обратно в этот же плагин (заказ с оплатой его же способом) |
plugin: busy |
плагин был занят другим вызовом дольше, чем время этого вызова, — держите задачи cron короткими |
panel.api: answer over 4 MB — page with limit/offset |
используйте limit/offset или {blob: true} |
status: 403 от panel.api |
у плагина нет права на этот маршрут — добавьте его в permissions |
pdb: statement not allowed: PRAGMA … |
запрещённая команда; транзакции — через panel.db.tx |
pdb: database is over its quota |
база дошла до db_quota_mb. Удалите старые строки (их место используется снова) или поднимите квоту |
kv value over 65536 bytes |
значение больше 64 КБ — храните в таблице |
blob "…": not a blob of this call |
ручка от другого вызова; blob живёт один вызов |
main.js does not export what plugin.json declares: … |
в provides объявлено то, чего main.js не экспортирует (проверьте имя и export) |
plugin: fill in the settings first: … |
не заполнены обязательные настройки |
provides.X needs the "…" permission: … |
точке нужно право — добавьте его в permissions |
provides.X: not available in this panel version yet |
точка появилась в более новой панели — поднимите panel в манифесте и обновите панель |
provides.X is experimental: add "X" to "experimental" |
добавьте точку в experimental |
a paid payment must report amount_kopecks and currency |
payment.status/webhook вернул paid без суммы |
payment.create must return provider_id and an http(s) pay_url |
неполный ответ create |
migrations: 0001_init.sql was changed — add a new migration instead |
выпущенную миграцию изменили — верните её и добавьте новую |
Вопросы¶
Можно ли npm-пакеты, TypeScript? Да, если собрать всё в один ES-модуль main.js
(esbuild: esbuild src/index.ts --bundle --format=esm --outfile=main.js). Пакеты, которым
нужен Node (fs, net, process, Buffer), работать не будут. Сеть — только через
panel.http.fetch.
Что есть из JavaScript? Язык целиком, включая свежие стандарты: классы,
async/await, Map, Set, BigInt, WeakRef, Array.prototype.at/findLast/toSorted,
Object.groupBy, JSON, Date, Math, RegExp, а также atob/btoa. Нет того, что дают
браузер или Node: setTimeout/setInterval, fetch (есть panel.http.fetch), URL и
URLSearchParams (собирайте строку с encodeURIComponent), TextEncoder/TextDecoder,
Intl (toLocaleString не форматирует по языку), structuredClone, require/import
других файлов.
Как хранить данные дольше одного вызова? В panel.kv или panel.db. Глобальные
переменные пропадают при перезапуске VM.
Как сделать что-то позже или регулярно? Объявите задачу в cron и храните очередь в
своей базе.
Видит ли плагин данные других плагинов? Нет. Свою базу, свои настройки и свой kv видит
только он.
Что будет при удалении? Плагин и его база удаляются. Оператор может удалить плагин, оставив базу (Удалить → оставить данные): установка того же плагина позже её подхватит.
Работает ли плагин на нодах? Нет, только на главном сервере. Ноды он видит через
panel.api (/v1/nodes, право servers.view).
Можно ли поменять id? Нет. Новый id — это новый плагин с пустой базой.