Skip to content

Лимиты, безопасность и частые ошибки

English version · Оглавление

Как устроена песочница

  • 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 — это новый плагин с пустой базой.