Плагин — дополнительные возможности площадки. Всё, что разрешает API площадки, можно сделать в плагине: Lovsel не сводит плагины к набору готовых функций. Если площадка позволяет действие, разработчик может реализовать его в своём плагине — готовым методом SDK или прямым запросом к API площадки через подключение магазина в Lovsel.
Это руководство для разработчика, который впервые видит Lovsel — кабинет продавцов на площадках GGsel, FunPay и Playerok: как устроен плагин, что даёт ядро, как обращаться к площадке, как построить интерфейс, который выглядит частью Lovsel, как проверить плагин и выпустить его в магазин.
Руководство общее для всех площадок. Отдельные статьи:
- «Создаём плагин как KOSell: связки товаров» — пошаговый урок: полноценный плагин с выбором товаров из каталога, мастером, списком с переключателями, статистикой и публикацией;
- «Компоненты интерфейса SDK» — все блоки: показатели, графики, таблицы, выбор товаров, редактор, настройки, мастера, предпросмотр сообщений;
- «Разработка, публикация и проверка Lovsel» — тестовая копия, сборки, публикация, отметка «Проверен Lovsel»;
- «Плагины для GGsel», «Плагины для FunPay» и «Плагины для Playerok» — готовые методы, прямой доступ к API, события и полные примеры для каждой площадки;
- «Единый баланс Lovsel в плагинах» — оплата закупок по заказам и ключи сервисов.
Главное правило интерфейса. Плагин описывает данные и действия, а Lovsel превращает их в веб-интерфейс: карточки, таблицы, формы, прогресс. Не стройте плагин вокруг Telegram-кнопок и текстовых меню.
Принцип: всё, что разрешает API площадки
Плагин работает от имени магазина продавца через подключение, которое продавец уже настроил в Lovsel. Доступ к площадке устроен в два уровня:
- Готовые методы —
ctx.ggsel.orders(),ctx.funpay.send_message(),ctx.playerok.deliver()и другие. Они берут сохранённые данные Lovsel (быстро и без лишних запросов к площадке), учитывают особенности площадки и защищают от повторов: одна выдача на сделку, сверка действия, ответ которого потерялся. - Прямой доступ к API —
ctx.ggsel.request(),ctx.funpay.request()иctx.playerok.graphql(). Любой метод, запрос или действие, которое разрешает площадка, плагин выполняет сам — даже если для него нет готового метода. Подробно — в разделе «Прямой доступ к API площадки».
Что при этом делает Lovsel:
- хранит ключи доступа и передаёт их площадке сам. API-ключ GGsel, golden_key FunPay, cookies и токен Playerok хранятся на сервере Lovsel и плагину не передаются: плагин получает ответы площадки, а не ключи. Lovsel входит в API, обновляет токен и сессию, соблюдает паузы между запросами;
- проверяет площадку. Плагин работает только со своей площадкой (
MANIFEST["marketplace"]); - показывает покупателю разрешения. Разрешения манифеста — честное объявление того, что плагин делает с магазином. Методы SDK сверяют вызов с объявленным разрешением, но не ограничивают то, что разрешает API: разрешение
marketplace.api(«Полный доступ к API площадки») открывает всё сразу; - требует активную подписку. Плагины работают, пока у продавца активна подписка Lovsel.
Ограничения плагина — только ограничения самой площадки (чего нет в её API, того нет и в плагине) и правила безопасности Lovsel: плагин не читает файлы и настройки сервиса, не видит данные других продавцов и не запускает программы на сервере (см. «Безопасность»).
Что такое плагин
Плагин — один файл .py, который расширяет кабинет продавца Lovsel: работает с заказами и товарами на площадках GGsel, FunPay и Playerok, подключает поставщика, меняет цены, выдаёт товары, пишет покупателям, строит отчёты. У каждого продавца своя копия плагина со своими настройками и данными.
Плагин может:
- показывать собственный раздел в кабинете Lovsel — со своими вкладками, показателями, графиками, таблицами, формами и мастерами;
- выполнять действия по кнопкам и формам;
- запускать длительные операции с живым прогрессом;
- реагировать на события площадок: новый заказ, новое сообщение, смена статуса, отзыв;
- выполнять на своей площадке — GGsel, FunPay или Playerok — любое действие, которое разрешает её API;
- хранить настройки и собственные данные;
- обращаться к внешним сервисам: поставщикам, своим серверам;
- оплачивать закупки по заказам с единого баланса Lovsel;
- принимать файлы от пользователя и отдавать файлы на скачивание;
- отправлять уведомления в центр событий Lovsel и в Telegram продавца.
Каждый плагин работает с одной площадкой: GGsel, FunPay или Playerok. Каталог плагинов у каждой площадки свой. Если нужна та же функция на другой площадке — это отдельный плагин со своим манифестом.
Плагин работает на сервере Lovsel, пока у продавца активна подписка.
Быстрый старт
Минимальный рабочий плагин — около 25 строк. Этот сделан для FunPay; для GGsel или Playerok поменяйте marketplace и обращайтесь к ctx.ggsel или ctx.playerok:
from lovsel_sdk import WebPlugin, ui
MANIFEST = {
"id": "acme.hello",
"name": "Привет, Lovsel",
"version": "1.0.0",
"author": "ACME",
"description": "Показывает число заказов FunPay.",
"icon": "👋",
"category": "tools",
"marketplace": "funpay",
"permissions": ["orders.read"],
}
app = WebPlugin(MANIFEST)
@app.page("overview", "Обзор", icon="layout-dashboard")
def overview(ctx, params):
if not ctx.funpay.connected:
return [ui.empty("FunPay не подключён", "Подключите аккаунт FunPay — плагин заработает сам.")]
return [ui.stats([ui.stat("Заказов FunPay", len(ctx.funpay.orders()))]),
ui.card("Приветствие", [ui.text("Здравствуйте! Магазин: **" + ctx.shop_title + "**")])]Как попробовать:
- Сохраните код в файл
hello.py. - В Lovsel откройте Плагины → Разработчикам → Создать плагин, выберите файл и нажмите «Создать и тестировать у себя».
- Плагин сразу появится в меню кабинета и откроется на отдельной странице с вкладкой «Обзор». Модерация для этого не нужна.
Дальше — урок «Создаём плагин как KOSell: связки товаров»: по шагам от пустого файла до плагина с каталогом товаров, мастером, списком, статистикой и настройками. Другие полные примеры: «Благодарности покупателям» для GGsel и «Отзывы и напоминания» для FunPay. Пример со всеми возможностями ядра — настройками, операцией и файлом — docs/examples/example_web_plugin.py.
Как плагин живёт в Lovsel
- Загрузка. Lovsel читает
MANIFESTбез выполнения кода, проверяет версию, разрешения и площадку, затем импортирует файл. Ошибка импорта видна во вкладке «Разработка» кабинета разработчика и в карточке плагина. - Запуск. Для каждого продавца создаётся свой контекст
ctx, подписываются события@app.on, вызываются функции@app.on_start(миграция данных, фоновые задачи). Плагин только для FunPay или только для Playerok ждёт, пока продавец подключит площадку. - Работа. Открыл продавец вкладку — вызывается функция страницы
@app.page(до 15 секунд), нажал кнопку, отправил форму, переключил связку или ищет товар — вызывается действие@app.action(до 20 секунд). Пришёл заказ — вызывается обработчик события. Всё долгое — в операцииctx.operations. - Выключение и обновление. Выключенный плагин не получает событий, его разделы закрыты, данные сохраняются. Новая версия заменяет код, а
ctx.storageи настройки остаются — переводите старые данные в@app.on_start(см. «Обновление плагина»). - Удаление. Продавец удалил плагин — файл плагина убирается из его кабинета, события и действия больше не вызываются.
Структура файла
from lovsel_sdk import WebPlugin, ui, ValidationError
MANIFEST = {...} # 1. описание плагина
app = WebPlugin(MANIFEST) # 2. объект плагина (обязательно с именем app)
app.settings([...]) # 3. настройки (необязательно)
@app.page("overview", "Обзор") # 4. страницы — порядок объявления = порядок вкладок
def overview(ctx, params): ...
@app.action("sync") # 5. действия — вызываются кнопками и формами
def sync(ctx, data): ...
@app.on("new_order") # 6. события магазина (необязательно)
def on_order(ctx, event): ...
@app.on_start # 7. запуск фоновых задач (необязательно)
def start(ctx): ...Первый аргумент каждого обработчика — ctx, контекст конкретного магазина. Никогда не храните данные магазина в глобальных переменных модуля: у каждого продавца своя копия плагина, но глобальные объекты удобно перепутать между потоками.
Сервер читает MANIFEST без выполнения кода (через разбор Python-файла), поэтому манифест должен быть обычным словарём из строк, чисел и списков — без вызовов функций и переменных.
Манифест
| Поле | Обязательно | Описание |
|---|---|---|
id | да | Уникальный идентификатор, например studio.supplier-sync. Не меняйте между версиями. |
name | да | Название в каталоге и в меню (до 120 символов). |
version | да | Версия в формате 1.2.3. Каждая новая выпущенная версия — больше предыдущей. |
author | нет | Подпись для плагина вне магазина. В магазине автором всегда показывается ник вашего аккаунта Lovsel — его меняют в «Настройки → Профиль». |
description | да | Одно-два предложения: что делает плагин. |
icon | нет | В магазине не используется: иконку-картинку загружают в карточке плагина. Без неё показывается логотип площадки плагина — GGsel, FunPay или Playerok. |
category | нет | Одна из категорий ниже. |
permissions | да | Разрешения, которые нужны плагину. Пустой список — если плагин не трогает магазин. |
marketplace | да | Площадка плагина — ровно одна: "ggsel", "funpay" или "playerok". Плагин без площадки или с несколькими Lovsel не примет. См. «Площадка плагина». |
capabilities | нет | Что умеет плагин (для карточки каталога). |
homepage | нет | Ссылка на сайт или поддержку (https://…). |
Категории: automation (автоматизация), pricing (цены), delivery (выдача товаров), suppliers (поставщики), messages (сообщения), analytics (аналитика), tools (инструменты), other.
Разрешения
| Разрешение | Что открывает |
|---|---|
orders.read | история продаж, подробности заказа, проверка кода покупки |
orders.write | действия с заказами: отметка «Товар отправлен», возврат денег покупателю |
chats.read | переписка с покупателем |
chats.write | сообщения и изображения покупателям, отметка о прочтении |
products.read | список, карточки и поля товаров |
products.write | изменение цены, остатка, описания, публикация, снятие и удаление товаров |
reviews.read | отзывы покупателей |
reviews.write | ответы продавца на отзывы |
balance.read | баланс продавца на площадке |
marketplace.api | полный доступ к API площадки: любые действия, которые разрешает API площадки, от имени магазина. Включает все разрешения выше и прямые запросы ctx.ggsel.request, ctx.funpay.request, ctx.playerok.graphql |
storage | собственное хранилище (объявляйте, если используете ctx.storage) |
network | запросы во внешние сервисы через ctx.http — «Запросы во внешние сервисы» |
files | приём и выдача файлов |
notifications | уведомления в центр событий |
wallet | оплата закупок по заказам с единого баланса Lovsel: резерв, списание, снятие резерва, возврат — «Единый баланс» |
Разрешения — честное объявление для покупателя: он видит их до покупки, а проверка Lovsel сверяет их с кодом. Методы ctx.ggsel.*, ctx.funpay.* и ctx.playerok.* работают с объявленными разрешениями — иначе выбрасывается PermissionDenied с понятным текстом, что добавить в манифест. Ограничения функциональности в этом нет: нужны любые действия, которые разрешает API площадки, — объявите marketplace.api. Названия разрешений одинаковы для всех площадок: orders.read у плагина GGsel открывает заказы GGsel, у плагина FunPay — заказы FunPay, у плагина Playerok — сделки Playerok.
Объявляйте то, что плагин действительно делает: узкие разрешения (orders.read, chats.write) понятнее покупателю, чем полный доступ. marketplace.api уместен, когда плагину нужны прямые запросы к API или много разных действий.
Возможности (capabilities): web_ui, settings, background, events, operations, telegram. web_ui и settings Lovsel определяет сам.
Площадка плагина
Lovsel работает с площадками GGsel, FunPay и Playerok. Каждый плагин работает с одной площадкой: GGsel, FunPay или Playerok. Каталог плагинов у каждой площадки свой. Площадку объявляет поле marketplace:
MANIFEST = {
"id": "studio.funpay-helper",
"name": "Помощник FunPay",
"version": "1.0.0",
"author": "Studio",
"description": "Отвечает покупателям FunPay и следит за лотами.",
"permissions": ["orders.read", "chats.read", "chats.write", "products.read"],
"marketplace": "funpay", # одна площадка: "ggsel", "funpay" или "playerok"
}- Площадка — ровно одна. Плагин без площадки или с несколькими Lovsel не примет ни при создании, ни при загрузке новой сборки, ни при загрузке своего плагина: «Плагин работает только с одной площадкой: укажите одну — ggsel, funpay или playerok». Прежняя запись списком из одной площадки,
"marketplaces": ["funpay"], тоже принимается. - Площадку задаёт только манифест. Карточка каталога берёт её из выпущенной сборки, и плагин появляется в каталоге своей площадки: покупатель видит его, когда выбрал эту площадку вверху кабинета.
- Нужна та же функция на другой площадке — сделайте отдельный плагин: общий код можно держать одинаковым, меняются
marketplace,idи обращения кctx.ggsel,ctx.funpayилиctx.playerok. - Плагин FunPay у покупателя не запускается, пока FunPay не подключён: в кабинете он отмечен «ждёт подключения» и включается сам сразу после подключения аккаунта. Так же ведёт себя плагин Playerok.
- Плагин работает только со своей площадкой.
ctx.ggselдоступен только плагину GGsel (иначеPermissionDenied) — и готовые методы, и прямые запросы; события GGsel (new_order,new_message…) приходят только ему, событияfunpay.*— только плагину FunPay,playerok.*— только плагину Playerok. Событиеinitполучают все. - В меню кабинета покупатель видит плагины выбранной площадки; в режиме «Все площадки» — все установленные.
- Плагины, опубликованные до этого правила с несколькими площадками, продолжают работать у покупателей; в каталоге и меню они стоят у первой из своих площадок (сначала GGsel, затем FunPay, затем Playerok). Новая сборка такого плагина должна объявить одну площадку.
- Интерфейс плагина окрашивается в цвет площадки: GGsel — зелёный, FunPay — голубой, Playerok — синий. Подробнее — в статье «Компоненты интерфейса SDK».
Что даёт каждая площадка:
| GGsel | FunPay | Playerok | |
|---|---|---|---|
| Доступ из плагина | ctx.ggsel | ctx.funpay | ctx.playerok |
| Заказы | orders(), order(), currency(), check_code() | orders(), order(), refund() | orders(), order(), deliver(), delivery(), mark_sent(), refund() |
| Переписка | chats(), messages(), send_message(), mark_read() — диалог заказа | chats(), messages(), send_message(), send_image() — диалог с покупателем | chats(), messages(), send_message(), send_image() — чат с покупателем |
| Товары | products(), product(), set_price(), update_product(), set_active(), delete_product() | lots(), refresh_lots(), lot_fields(), save_lot(), set_lot_price(), set_lot_active(), raise_lots() | items(), set_item_price(), update_item(), remove_item(), publish_options(), publish_item() |
| Прочее | reviews(), balance(), seller_id | reply_review(), balance(), username | reviews(), balance(), username |
| Прямой доступ к API | request() — Seller API v1 и v2 | request() — сайт FunPay от имени аккаунта | graphql() — GraphQL сайта Playerok |
| События | new_order, order_status, new_chat, new_message | funpay.new_order, funpay.order_status, funpay.new_chat, funpay.new_message, funpay.new_review | playerok.new_order, playerok.order_status, playerok.deal_problem, playerok.new_chat, playerok.new_message, playerok.new_review, playerok.delivery |
| Подробно | Плагины для GGsel | Плагины для FunPay | Плагины для Playerok |
Контекст ctx
| Свойство или метод | Назначение |
|---|---|
ctx.ggsel | Магазин GGsel: готовые методы и request() — API для GGsel. |
ctx.funpay | Аккаунт FunPay: готовые методы и request() — API для FunPay. |
ctx.playerok | Аккаунт Playerok: готовые методы и graphql() — API для Playerok. |
ctx.marketplace | Площадка плагина: "ggsel", "funpay" или "playerok". |
ctx.marketplaces | Та же площадка списком из одного элемента (у старых плагинов с несколькими площадками — все). |
ctx.settings | Словарь значений настроек (с учётом значений по умолчанию). |
ctx.save_settings(values) | Сохранить настройки программно. |
ctx.storage | Хранилище плагина: get(key, default), set(key, value), delete(key), all(), incr(key). Значения — любые JSON-данные до 1 МБ. |
ctx.operations | Длительные операции: start(title, total), get(id), active(). |
ctx.http | Запросы во внешние сервисы (разрешение network) — сессия как requests.Session. Подробно. |
ctx.wallet | Единый баланс продавца: balance(), reserve(), purchase(), resume() — только под конкретный заказ и с разрешением wallet. «Единый баланс Lovsel в плагинах». |
ctx.secrets | Ключи сервисов, которые администратор Lovsel задал плагину на сервере: get(name), require(name). |
ctx.notify(title, body, ...) | Событие в центр событий и Telegram продавца. |
ctx.log(message, level) | Запись в журнал плагина (info, success, warning, error). |
ctx.log_error(message, exc) | Ошибка в журнал вместе с трассировкой до строки вашего файла. |
ctx.download(name, content, mime) | Подготовить файл к скачиванию. |
ctx.user_id, ctx.shop_title | Идентификатор аккаунта и название магазина. |
ctx.core | Внутреннее ядро магазина. Только для совместимости со старыми плагинами: API не гарантируется, а проверка Lovsel отмечает такие обращения. Всё, что разрешает площадка, делается прямым доступом к API — ядро для этого не нужно. |
Хранилище изолировано: разные магазины не видят данные друг друга, а при обновлении версии плагина данные сохраняются.
Прямой доступ к API площадки
Готовых методов достаточно для большинства плагинов. Когда нужен метод, запрос или действие, для которого готового метода нет, плагин обращается к API площадки напрямую. Нужно разрешение marketplace.api — покупатель видит его как «Полный доступ к API площадки».
| Площадка | Метод | Что это |
|---|---|---|
| GGsel | ctx.ggsel.request(method, path, params=, json=, version="v1", headers=) | Любой метод GGsel Seller API. Список методов — официальная документация seller.ggsel.com/docs. |
| FunPay | ctx.funpay.request(method, path, params=, data=, json=, files=, ajax=False, csrf=True) | У FunPay нет публичного API: запрос к сайту funpay.com через сессию аккаунта — всё, что продавец делает на сайте. |
| Playerok | ctx.playerok.graphql(query, variables, operation_name, persisted_hash=) | Любой запрос и действие GraphQL сайта playerok.com от имени аккаунта. |
# GGsel: последние продажи — Seller API v1, токен входа подставляет Lovsel
sales = ctx.ggsel.request("GET", "/seller-last-sales",
params={"seller_id": ctx.ggsel.seller_id, "top": 50, "group": "false"})
# GGsel: снять оффер с продажи — Seller API v2, API-ключ в заголовке ставит Lovsel
ctx.ggsel.request("POST", "/offers/batch_pause", json={"offer_ids": [101]}, version="v2")
# FunPay: страница «Мои продажи» — как её видит продавец в браузере
page = ctx.funpay.request("GET", "orders/trade")
# Playerok: запрос GraphQL от имени продавца
me = ctx.playerok.graphql("query viewer { viewer { id username } }")Что делает Lovsel при прямом запросе:
- подставляет доступ: токен входа и API-ключ GGsel, cookies и User-Agent сессии FunPay, cookies, заголовки браузера и прокси Playerok — значения плагину не передаются;
- заново входит в API, если токен или сессия истекли;
- не отправляет данные сессии на другие сайты: адрес FunPay — только funpay.com, а путь GGsel — только от адреса Seller API; cookies сессии в ответ не попадают;
- повторяет чтения при обрыве связи, а действия не повторяет: если ответ на действие потерялся, приходит
MarketplaceErrorс кодомunknown_result— сначала проверьте результат на площадке, потом решайте; - соблюдает паузы между запросами к Playerok, общие со встроенной синхронизацией.
Ошибки площадки приходят исключением MarketplaceError:
| Поле | Значение |
|---|---|
marketplace | "ggsel", "funpay" или "playerok" |
status | HTTP-код ответа или None, если ответа нет |
code | network — нет связи, auth — площадка не приняла вход, blocked — запрос остановила защита сайта, rate_limited — слишком часто, http — ответ с ошибкой, graphql — ошибка запроса Playerok, bad_response — ответ не в ожидаемом формате, not_found — объект не найден, unknown_result — исход действия неизвестен; у отдельных действий — свои коды: price_changed, bad_state, foreign_url |
message | Текст для продавца — его можно показать как есть |
body | Начало ответа площадки, если он был |
from lovsel_sdk import MarketplaceError, ValidationError
try:
ctx.ggsel.request("PATCH", "/offers/101", json={"quantity": 10}, version="v2")
except MarketplaceError as exc:
if exc.unknown_result:
ctx.log("GGsel не ответил — проверим остаток позже", "warning")
else:
raise ValidationError("GGsel не принял изменение: " + exc.message)ctx.funpay.request возвращает ответ MarketResponse с любым HTTP-кодом: status, ok, url, headers, text, content, json() и raise_for_status() — как у requests.
Прямой запрос — то же действие от имени магазина, что и готовый метод: спрашивайте подтверждение перед всем, что меняет магазин или тратит деньги, и не опрашивайте площадку в цикле без паузы. Примеры для каждой площадки — в разделах «Прямой доступ к API» статей для GGsel, для FunPay и для Playerok.
Страницы и навигация
У плагина свой раздел в кабинете: шапка со значком, названием, состоянием и площадками, под ней — вкладки страниц плагина. Второй боковой панели у плагина нет, в левом меню кабинета — только сами плагины, без их разделов. Страница — функция, которая возвращает список блоков. Lovsel отрисовывает их своими компонентами, поэтому интерфейс выглядит как встроенные разделы, поддерживает тёмную и светлую тему и телефоны.
app = WebPlugin(MANIFEST)
@app.page("products", "Товары", icon="package", group="Рабочее пространство",
description="Слоты, созданные плагином", badge=lambda ctx: len(ctx.storage.get("slots", [])) or None)
def products(ctx, params):
rows = ctx.storage.get("slots", [])
return [
ui.header("Товары", "Слоты, созданные плагином",
actions=[ui.button("Создать слоты", action="create_slots", variant="primary", icon="plus")]),
ui.table(
[ui.column("title", "Игра", subtitle="region"),
ui.column("price", "Цена", align="right", format="money",
currency=ctx.ggsel.currency()), # или поле "currency" в каждой строке
ui.column("state", "Статус", format="status")],
[dict(r, state=ui.badge("активен", "success", value="on")) for r in rows],
filters=[ui.filter("state", "Статус", [("on", "Активные"), ("off", "Выключенные")], style="tabs")],
searchable=True,
row_click=ui.button("Открыть", page="slot"),
row_menu=[ui.button("Удалить", "delete_slot", icon="trash", danger=True, confirm="Удалить слот?")],
empty_text="Слотов ещё нет",
),
]
@app.page("slot", "Слот", hidden=True) # карточка записи: открывается из строки таблицы
def slot(ctx, params):
...- Страницы показываются вкладками в порядке объявления;
group— группа вкладок (между группами — разделитель),badge— счётчик,hidden=True— страница без вкладки (карточка записи, мастер),bottom=True— вкладка в конце строки. app.section("catalog", "Каталог")— раздел-папка, страницы попадают в него черезparent="catalog"и показываются второй строкой вкладок; разделы вкладываются друг в друга (до трёх уровней).@app.menu_badge— счётчик у самого плагина в меню кабинета. Подробно и с примерами — в разделе «Навигация плагина».- Параметр
layoutуWebPluginоставлен для совместимости: прежние плагины сlayout="sidebar"загружаются без изменений и тоже показываются вкладками. - Второй аргумент страницы —
params: параметры из кнопки (ui.button(page=..., params=...)), строки таблицы (params["id"]), поиска и фильтров панелиui.toolbar, вкладок. Они хранятся в адресе страницы. - Функция страницы должна отработать быстро (до 15 секунд). Всё долгое — в операции. Страницы стройте по сохранённым данным (
orders(),lots(),items(),ctx.storage), а запросы к площадке делайте по действию пользователя.
Основные блоки:
| Блок | Назначение |
|---|---|
ui.header, ui.card, ui.grid, ui.section, ui.tabs, ui.details | Шапка страницы и раскладка. |
ui.stats, ui.stat(spark=, glow=) | Плитки показателей с изменением и линией динамики — как на главной кабинета. |
ui.chart, ui.series | Графики как в «Аналитике»: несколько линий, цвета площадок, виды «Гладкий / Острый / Столбцы». |
ui.keyvalue, ui.list_view, ui.timeline, ui.steps | Списки, ленты и этапы. |
ui.table, ui.column(format="switch"), ui.toolbar, ui.search, ui.filter | Таблицы с поиском, вкладками-фильтрами, переключателями в строках, выбором строк и меню действий. |
ui.field.picker, ui.choice, ui.choices | Выбор товаров поиском по мере ввода или из визуального каталога, один или несколько. |
ui.editor | Редактор объекта в панели справа: поля, «Сохранить», «Удалить». |
ui.settings, ui.settings_group | Настройки по группам с автосохранением. |
ui.form, ui.wizard, ui.field.* | Формы и пошаговые мастера; поля суммы field.money и срока field.duration. |
ui.field.template, ui.message_preview, ui.chat | Текст сообщения с переменными и предпросмотром в чате площадки. |
ui.button, ui.menu, ui.actions, ui.toggle | Кнопки, меню и переключатели. |
ui.text, ui.alert, ui.code, ui.empty, ui.badge, ui.money | Текст, сообщения, значения. |
ui.asset, ui.image, ui.banner, ui.gallery | Ваши картинки: баннеры, превью, скриншоты и шаги инструкций. |
ui.progress, ui.operations, ui.logs | Прогресс, операции и журнал. |
Все параметры блоков, форматы колонок, варианты кнопок и готовые образцы страниц — в статье «Компоненты интерфейса SDK».
Кнопки и блоки в разделах кабинета
Плагин может встроить свою кнопку или блок с данными в разделы Lovsel: действие над выбранными товарами в списке товаров ("products.bulk"), блок и кнопки в карточке товара ("product.card") или заказа ("order.card").
app.embed_button("products.bulk", "Удалить слоты", "clear_slots", icon="trash", tone="danger",
confirm="Удалить слоты выбранных товаров?") # data["ids"] — выбранные товары
@app.embed_block("order.card", "SMM Prime", icon="trending-up")
def smm_in_order(ctx, target): # target["id"] — номер заказа
job = ctx.storage.get("jobs", {}).get(target["id"])
return [ui.keyvalue([("Статус", job["status"])])] if job else NoneМеста, данные, ограничения по времени и проверка в тестовом стенде (plugin.embed) — в разделе «Встраивание в разделы кабинета».
Действия и кнопки
@app.action("reprice", confirm="Пересчитать цены всех слотов?")
def reprice(ctx, data):
row = data.get("row") # строка таблицы, если кнопка в row_actions или row_menu
...
return ui.result("Цена обновлена")data— словарь:payloadкнопки, значения формы, строка таблицы (row), выбранные строки (ids,rows) и загруженные файлы. Переключатель в строке таблицы передаётrow,keyиvalue, редакторui.editor— значения полей иidзаписи, поискfield.picker—query,field,pageиvaluesформы.- Действие должно завершиться за 20 секунд. Если нужно дольше — запустите операцию и сразу верните результат.
confirmв кнопке или действии показывает диалог подтверждения. Используйте его для всего, что меняет магазин или удаляет данные.
Что можно вернуть:
| Результат | Эффект |
|---|---|
None | Страница обновится. |
"Текст" | Уведомление «успешно» и обновление страницы. |
ui.result("Текст", tone="info") | Уведомление: success, info, warning, danger. |
ui.result(operation=op) | Показать прогресс запущенной операции. |
ui.result(page="orders", params={"id": 5}) | Перейти на страницу плагина. |
ui.result(dialog=ui.dialog("Итог", [blocks], side=True)) | Окно с результатом; side=True — панель справа. |
ui.result(download=ctx.download(...)) | Скачать файл. |
ui.result(values={...}) | Новые значения полей формы. |
ui.result(refresh=False) | Не перезагружать страницу. |
ui.choices([ui.choice(...)], total=) | Варианты для поиска в поле выбора товаров. |
Ошибку ввода покажите у поля:
from lovsel_sdk import ValidationError
if price <= 0:
raise ValidationError({"price": "Цена должна быть больше нуля"})Любое другое исключение Lovsel покажет пользователю как «Ошибка плагина: …» и запишет в журнал с трассировкой. Текст исключения должен быть понятен продавцу.
Формы и пользовательский ввод
ui.form("add_slot", [
ui.field.text("title", "Название игры", required=True, width="half"),
ui.field.select("region", "Регион", [("ar", "Аргентина"), ("tr", "Турция")], default="ar", width="half"),
ui.field.number("markup", "Наценка", min=0, max=200, default=15, suffix="%"),
ui.field.switch("auto", "Обновлять цену автоматически", default=True),
ui.field.textarea("note", "Комментарий", show_if={"field": "auto", "equals": False}),
ui.field.text("token", "Ключ поставщика", secret=True),
ui.field.date("since", "Начиная с даты"),
ui.field.file("keys", "Файл с ключами", accept=".txt", max_mb=5),
], submit="Добавить", action="add_slot")Lovsel проверяет обязательные поля, числа и списки ещё до вызова действия и приводит значения к типам: number → число, money → сумма до копеек, duration → {"value": 7, "unit": "days", "minutes": 10080}, picker → id или список id, switch → True/False. Для длинных форм есть ui.wizard — пошаговый мастер с проверкой каждого шага. Все поля — radio, checkboxes, tags, template, список с поиском, выбор товаров из каталога — описаны в статье «Компоненты интерфейса SDK».
Файл из формы приходит объектом UploadedFile: name, size, mime, content (байты), text(), lines().
@app.action("add_slot")
def add_slot(ctx, data):
keys = data.get("keys")
lines = keys.lines() if keys else []
...Не просите пользователя вводить данные по одному сообщению («введите название», «теперь введите цену»). Одна форма — одно действие.
Настройки
def check(ctx, values):
if values["markup"] > 100 and not values["confirm_high"]:
raise ValidationError({"markup": "Больше 100% — включите подтверждение"})
return values # можно вернуть исправленные значения
app.settings([
ui.field.text("api_key", "API-ключ поставщика", secret=True, required=True),
ui.field.number("markup", "Наценка", min=0, max=500, default=15, suffix="%"),
ui.field.switch("confirm_high", "Разрешить наценку больше 100%"),
], description="Ключ хранится только на сервере Lovsel.", validate=check)Lovsel создаст служебную вкладку «Настройки». Поля secret не отдаются в браузер: пустое поле при сохранении означает «оставить прежнее значение». Читайте настройки через ctx.settings["markup"].
Если настроек много и их удобно разбить по смыслу, сделайте свою страницу с панелью ui.settings — группы слева, поля справа, изменения сохраняются сами. Пример — в уроке и в разделе «Панель настроек».
Хранение данных
ctx.storage — постоянное хранилище «ключ → значение JSON» у каждого продавца (разрешение storage). Держите объекты по смыслу под отдельными ключами: bundles — словарь записей по id, rentals — список с ограничением длины, settings — свои настройки.
bundles = ctx.storage.get("bundles", {}) or {}
bundles[bundle_id] = {"name": "Steam на неделю", "price": 299, "enabled": True}
ctx.storage.set("bundles", bundles)
ctx.storage.incr("sent_today") # счётчик
ctx.storage.delete("draft") # удалить ключ
everything = ctx.storage.all() # все ключи плагина- Значение — до 1 МБ JSON. Длинные журналы обрезайте:
ctx.storage.set("rentals", rentals[:2000]). - Одновременно с действием может прийти событие заказа: читайте данные из хранилища непосредственно перед изменением и сразу сохраняйте, не держите их между вызовами.
ctx.settings— значенияapp.settings([...]), они хранятся отдельно и редактируются на странице «Настройки», которую строит Lovsel. Для своей страницы настроек с группами используйтеui.settingsиctx.storage.
Длительные операции и прогресс
Всё, что дольше пары секунд, оформляйте операцией. Пользователь видит, что происходит, сколько готово и сколько осталось, и может остановить операцию.
@app.action("create_slots", confirm="Создать слоты для выбранных игр?")
def create_slots(ctx, data):
games = load_games(ctx)
op = ctx.operations.start("Создание слотов", total=len(games))
def work(op):
for game in games:
if op.cancelled:
return "Остановлено пользователем"
op.update(stage="Создание", current=game["title"] + " / " + game["region"])
try:
create_slot(ctx, game)
op.step(ok=True)
except Exception as exc:
op.step(ok=False, note=game["title"] + ": " + str(exc))
op.finish("Создано: %d, ошибок: %d" % (op.success, op.errors))
op.run_in_background(work)
return ui.result("Создание запущено", operation=op)| Метод | Назначение |
|---|---|
op.update(done=, total=, current=, stage=, success=, errors=, message=) | Обновить прогресс. |
op.step(ok=True, current=, note=) | Отметить один обработанный элемент. |
op.log(message, level) | Запись в журнал операции. |
op.cancelled | Пользователь нажал «Остановить» — завершите работу. |
op.finish(message, result={"download": ...}) | Успешное завершение. |
op.fail(message) | Завершение с ошибкой. |
op.run_in_background(fn) | Выполнить fn(op) в фоне; исключение превратится в fail. |
Пользователь увидит карточку:
Создание слотов выполняется
14 / 50 28% · осталось ≈ 2 мин
[██████░░░░░░░░░░░░░░]
Создание: Сейчас Steam / Argentina
Успешно: 13 Ошибок: 1 Осталось: 36Прогресс отображается на странице плагина, в шапке кабинета и на обзоре. По завершении приходит уведомление в центр событий. Чтобы показать прогресс внутри своей карточки, используйте ui.progress(operation=op.id).
Ошибки и результаты
- Ошибка ввода —
ValidationErrorу конкретного поля. - Ошибка действия — исключение с понятным текстом или
ui.result("…", tone="danger"). - Площадка отказала или не ответила —
MarketplaceError: покажите продавцуexc.message, а приexc.unknown_resultсначала проверьте результат на площадке. - Проблема, требующая внимания продавца —
ctx.notify(title, body, level="error")илиneeds_action=True: событие попадёт в раздел «Требуют внимания». - Технические подробности —
ctx.log(..., "error")илиctx.log_error(message, exc), они видны во вкладке «Журнал». - Итог большой операции —
op.finish(message, result=...)и, при необходимости, диалогui.dialogс таблицей.
ctx.notify(
"Поставщик не ответил",
"Цены не обновлены 30 минут. Проверьте ключ API.",
level="error",
product="Steam Gift Card",
)Ошибки страниц, действий и обработчиков событий Lovsel записывает в журнал сам — с трассировкой до строки вашего файла. Их видно в пункте «Журнал» меню плагина; ошибка загрузки плагина видна и во вкладке «Разработка» кабинета разработчика.
События площадок
Плагин подписывается на события декоратором @app.on(...). Первый аргумент обработчика — ctx, второй — словарь события:
@app.on("new_order") # новый заказ GGsel
def on_new_order(ctx, event):
order = event["order"]
...
@app.on("funpay.new_message") # сообщение покупателя FunPay
def on_message(ctx, event):
text = event["message"]["text"]
...| Площадка | События | Данные и примеры |
|---|---|---|
| Все | init — магазин запущен | — |
| GGsel | new_order, order_status, new_chat, new_message | События GGsel |
| FunPay | funpay.new_order, funpay.order_status, funpay.new_chat, funpay.new_message, funpay.new_review | События FunPay |
| Playerok | playerok.new_order, playerok.order_status, playerok.deal_problem, playerok.new_chat, playerok.new_message, playerok.new_review, playerok.delivery | События Playerok |
| Баланс | wallet.operation — деньги под ждущий заказ зарезервированы или зависшую закупку пора проверить | Единый баланс |
В каждом событии есть поле marketplace ("ggsel", "funpay" или "playerok"). События площадки приходят только плагинам, объявившим эту площадку, и только при подключённом аккаунте.
Обработчик должен работать быстро. Долгую реакцию запускайте операцией или своим потоком из @app.on_start. Если вы запускаете поток, проверяйте флаг остановки и не держите блокировки. Нужны данные, которых нет в событии, — запросите их готовым методом или прямым запросом к API.
Файлы
Приём — поле ui.field.file. Выдача:
file = ctx.download("prices.csv", csv_text, "text/csv")
return ui.result("Отчёт готов", download=file)Файл хранится в памяти 30 минут. Для больших выгрузок формируйте файл в операции и передавайте его через op.finish(result={"download": file}) — на карточке операции появится кнопка «Скачать».
Баланс Lovsel и ключи сервисов
ctx.wallet— оплата закупок по заказам с единого баланса продавца в Lovsel (разрешениеwallet). Плагин не трогает деньги сам: он просит сервер зарезервировать сумму под заказ, списать её после покупки или снять резерв при ошибке. Схема защищает от двойных списаний при повторе события, перезапуске и зависшем запросе. Подробно — «Единый баланс Lovsel в плагинах».ctx.secrets— ключи сервисов плагина (например, API-ключ поставщика). Их задаёт администратор Lovsel в карточке плагина («Админ-панель → Маркетплейс → плагин → Ключи сервисов»); продавцы и разработчик значение не видят.ctx.secrets.require("SUPPLIER_API_KEY")— ключ или понятная ошибка, если его нет;ctx.secrets.get(name)— ключ илиNone.
Запросы во внешние сервисы: ctx.http
Поставщики, свои серверы, курсы валют — всё, что вне площадки, плагин запрашивает через ctx.http. Нужно разрешение network: без него ctx.http выбрасывает PermissionDenied, а покупатель видит «Запросы во внешние сервисы» на карточке плагина.
ctx.http устроен как requests.Session: методы get, post, put, patch, delete, head, request с параметрами params, data, json, headers, timeout; ответ — обычный requests.Response.
r = ctx.http.get("https://api.supplier.example/items/A1",
headers={"Authorization": "Bearer " + ctx.secrets.require("SUPPLIER_API_KEY")}, timeout=15)
r.raise_for_status()
price = r.json()["price"]
ctx.log("Цена поставщика получена " + ctx.http.route)- Маршрут. Без прокси запросы уходят напрямую с IP-адреса сервера Lovsel. Если администратор задал плагину исходящий прокси (служебный ключ
LOVSEL_HTTP_PROXYв «Ключах сервисов»; форматhttp://логин:пароль@host:portилиsocks5://логин:пароль@host:port), все запросыctx.httpидут через него. ctx.http.egress— откуда уходят запросы:"proxy"(через прокси плагина) или"server"(с IP-адреса сервера Lovsel).ctx.http.route— тот же маршрут словами, без логина и пароля, — удобно писать в журнал.- Время на запрос по умолчанию — 30 секунд. В действиях и на страницах ставьте
timeoutменьше их лимита (20 и 15 секунд), долгие запросы выполняйте в операции. - Обычный
requestsтоже работает, но такие запросы идут мимо прокси плагина. Запросы к поставщику с белым списком IP отправляйте только черезctx.http. - Не отправляйте во внешние сервисы данные покупателей сверх нужного для заказа и никогда — ключи площадок (их у плагина и нет).
Официальные плагины Lovsel: общий аккаунт поставщика
Официальный плагин Lovsel может закупать товар у сервиса-поставщика с одной учётной записью Lovsel: продавцу не нужно регистрироваться у поставщика, хранить свой ключ и пополнять там счёт. Схема:
- Ключ поставщика — на сервере Lovsel. Lovsel заводит аккаунт у поставщика, а администратор сохраняет его API-ключ в карточке плагина («Админ-панель → Маркетплейс → плагин → Ключи сервисов»). Плагин читает ключ через
ctx.secrets.require("SUPPLIER_API_KEY"); продавцы его не видят, в браузер он не попадает. - Продавец платит со своего баланса Lovsel. Под оплаченный заказ плагин резервирует сумму через
ctx.wallet(purchase()илиreserve()), покупает у поставщика с общего аккаунта и после покупки списывает резерв — закупка плюс комиссия Lovsel. Не хватает денег — заказ ждёт пополнения, ошибка поставщика — резерв снимается. Подробно — «Единый баланс Lovsel в плагинах». - Поставщик видит один IP-адрес. Все запросы к поставщику идут с сервера Lovsel, поэтому для поставщика это запросы с одного адреса — IP-адреса сервера Lovsel. Если у поставщика есть белый список IP, внесите в него этот адрес. Он показан в среде разработки официального плагина («Плагины → Разработчикам» → плагин), в панели «Расширенные настройки»; там же можно проверить, какой адрес видят внешние сервисы.
- Другой постоянный адрес — через прокси. Если поставщику нужен другой постоянный IP (например, он пускает только адреса своей страны), администратор задаёт исходящий прокси в той же панели «Расширенные настройки». Он хранится служебным ключом
LOVSEL_HTTP_PROXYв «Ключах сервисов» плагина, в белый список поставщика вносится адрес прокси. Через прокси идут только запросыctx.http— поэтому код плагина отправляет все запросы к поставщику черезctx.http, а не черезrequestsнапрямую.
MANIFEST = {
"id": "lovsel.supplier-steam",
"name": "Пополнение Steam",
"version": "1.0.0",
"description": "Оплаченный заказ → покупка у поставщика с аккаунта Lovsel → код покупателю.",
"marketplace": "ggsel",
"permissions": ["orders.read", "chats.write", "network", "wallet", "storage"],
}
app = WebPlugin(MANIFEST)
API = "https://api.supplier.example" # адрес API поставщика
@app.on("new_order")
def on_order(ctx, event):
order = event["order"]
def buy(op): # покупка с общего аккаунта Lovsel у поставщика
r = ctx.http.post(API + "/orders", json={"item_id": "steam-100", "client_order_id": op.id},
headers={"Authorization": "Bearer " + ctx.secrets.require("SUPPLIER_API_KEY")},
timeout=15)
r.raise_for_status()
return {"ref": str(r.json()["id"]), "goods": r.json()["code"]}
res = ctx.wallet.purchase(order["id"], 100, buy=buy, marketplace="ggsel", title="Steam 100 ₽",
supplier="Поставщик") # деньги — с баланса продавца в Lovsel
if res.ok:
ctx.ggsel.send_message(order["chat_id"], "Ваш код: " + res.result["goods"])
res.operation.mark_delivered()Перед выпуском проверьте в «Расширенных настройках», что маршрут и адрес те, которые внесены у поставщика, и что ctx.http.egress в журнале плагина показывает ожидаемый маршрут. Полный пример закупки с проверкой зависших запросов — docs/examples/supplier_purchase.py.
Стандарт интерфейса Lovsel
Проверка Lovsel учитывает эти правила, а покупатели ждут от плагина интерфейса, как у встроенных разделов.
- Вкладки по задачам. Типичный набор: «Обзор», рабочие страницы («Связки», «Правила», «Товары», «История»), «Настройки», в конце — «Инструкция». Lovsel сам добавляет «Журнал», «О плагине» и «Настройки» для
app.settings. Своей боковой панели у плагина нет. - Первая страница — обзор. Плитки
ui.statsс линией динамики, графикui.chart, «Требуют внимания» и одно главное действие. - Список объектов. Таблица с поиском и вкладками-фильтрами, переключатель «Включено» в строке (
format="switch"), изменение в редактореui.editor, удаление в меню «⋯» с подтверждением. Создание сложной записи — мастер на скрытой странице. - Пустые состояния. Если данных нет, объясните, что сделать:
ui.empty("Слотов нет", "Нажмите «Создать слоты»", action=...). - Одна основная кнопка на экран (
variant="primary"), остальные — обычные; опасные действия — в меню «⋯» илиui.details. - Подтверждение для всего, что меняет магазин, списывает деньги или удаляет данные — и для готовых методов, и для прямых запросов к API.
- Прогресс для любой работы дольше двух секунд.
- Таблицы вместо текстовых списков, формы и мастера вместо пошагового ввода, бейджи для статусов.
- Сообщения покупателю — с предпросмотром. Шаблоны редактируются полем
ui.field.template: продавец видит сообщение глазами покупателя. - Понятные тексты. Русский язык, без технических кодов, числа с единицами («15 %», «3 шт.»), суммы — через
ui.money(value, currency)или колонкуformat="money". Никогда не пишите знак валюты вручную: у площадок и заказов разная валюта — магазин GGsel может работать в долларах. - Без имитации Telegram. Не рисуйте меню из эмодзи-кнопок и не отправляйте экраны текстом.
- Секреты — только поля
secretиctx.secrets; не выводите ключи на страницах и в журнале. - Бережно к площадкам. Не опрашивайте GGsel, FunPay и Playerok в цикле без паузы: заказы, диалоги и товары уже есть в
orders(),chats(),lots()иitems(). Прямые запросы — по действию продавца, по событию или в операции с паузами. - Одна площадка. Плагин работает с одной площадкой: проверяйте
connectedперед обращением к ней и показывайте понятную подсказку, если она не подключена.
Проверка плагина
Без сервера. В SDK есть тестовый стенд. Он отрисовывает каждую страницу и проверяет блоки: опечатка в типе блока, кнопка без действия, повторяющийся ключ поля, выбор товаров или переключатель, который вызывает несуществующее действие, будут видны сразу.
python -m lovsel_sdk.testing my_plugin.py # манифест, навигация и отрисовка страниц
python -m lovsel_sdk.testing my_plugin.py --action exportВ автотестах:
from lovsel_sdk.testing import load_plugin
def test_overview():
plugin = load_plugin("my_plugin.py", orders=[{"id": "1", "title": "Ключ", "amount": 100}])
blocks = plugin.page("overview")
assert blocks[0]["type"] == "stats"
result = plugin.action("export", {})
assert result["operation"]["status"] == "success"Формы, мастера, редактор и настройки отправляйте методом plugin.submit — он проверяет и приводит значения теми же правилами, что сервер Lovsel:
fields = [f for step in plugin.page("new")[1]["steps"] for f in step["fields"]]
plugin.submit("create_bundle", fields, {"product_a": "ggsel:101", "product_b": "ggsel:102",
"price": "299,5", "term": "7d", "enabled": True})Товары и сообщения GGsel — параметр products: сообщения покупателям складываются в plugin.ggsel_sent. События вызываются методом plugin.emit:
plugin = load_plugin("my_plugin.py", products=[{"id": 101, "title": "Ключ Steam", "price": 199}])
plugin.emit("new_order", {"order": {"id": "9001", "product_id": 101, "chat_id": 9001}})
assert plugin.ggsel_sent[0]["chat_id"] == "9001"Плагин для FunPay проверяется с тестовым аккаунтом — сообщения не уходят на FunPay, а складываются в plugin.funpay_sent:
plugin = load_plugin("my_plugin.py", funpay={
"orders": [{"id": "AB12CD34", "title": "Ключ", "amount": 199, "status": "paid", "buyer": "anna", "chat_id": "700001"}],
"chats": [{"id": "700001", "buyer": "anna", "last_text": "Здравствуйте"}],
"lots": [{"id": "1001", "title": "Ключ", "price": 199}],
})
plugin.ctx.funpay.send_message("700001", "Спасибо!")
assert plugin.funpay_sent == [{"chat_id": "700001", "text": "Спасибо!"}]Прямые запросы на стенде отвечают из параметра api: ключ — "МЕТОД /путь" для GGsel и FunPay или имя операции GraphQL для Playerok, значение — ответ (словарь или список — JSON, строка — страница FunPay, (код, тело) — ответ с HTTP-кодом) или функция, которая получает запрос и возвращает ответ. Запросы плагина складываются в plugin.api_calls, действия готовых методов (возврат, ответ на отзыв, публикация) — в plugin.market_actions:
plugin = load_plugin("my_plugin.py", api={
"GET /seller-last-sales": {"retval": 0, "sales": [{"invoice_id": 9001, "amount": 100}]},
"PATCH /offers/101": lambda call: {"data": dict(call["json"], id=101)},
})
plugin.action("sync", {})
assert plugin.api_calls[0]["path"] == "/seller-last-sales"В Lovsel. Создайте плагин в «Плагины → Разработчикам → Создать плагин» — он сразу заработает у вас как тестовая копия. Во вкладке «Разработка» кабинета разработчика: «Открыть плагин», загрузка новой версии перетаскиванием файла, «Перезапустить», ошибка загрузки со строкой кода и картинки плагина. Нашли проблему — загрузите исправленный файл и сразу проверьте снова. Проверьте работу на телефоне и в тёмной теме. Тестовая копия работает с вашим настоящим магазином: прямые запросы и действия выполняются на площадке по-настоящему.
Изображения
Баннеры, превью и картинки для инструкций загрузите во вкладке «Разработка → Изображения» кабинета разработчика (PNG, JPEG, WebP или GIF до 2 МБ). В коде ссылайтесь на них по имени файла:
ui.banner(ui.asset("banner.png"), "Автовыдача за 3 шага", "Настройка займёт пару минут.")
ui.gallery([ui.asset("step-1.png"), ui.asset("step-2.png")], columns=2)
ui.image(ui.asset("result.png"), caption="Так покупатель увидит сообщение")Lovsel сам подставит адрес картинки, поэтому она одинаково открывается у вас и у покупателей, на компьютере и на телефоне. Подробно — в разделе «Изображения» справочника компонентов.
Публикация
Модерация, чтобы начать, не нужна: плагин работает у вас сразу после загрузки. Когда он готов:
- Нажмите «Опубликовать» в кабинете разработчика — бесплатно или по своей цене.
- Плагин появится в магазине, а выпущенная сборка уйдёт на проверку Lovsel.
- После проверки на карточке появится отметка «Проверен Lovsel».
Покупатели видят статус проверки, автора и разрешения до покупки. Как проходит проверка, что будет при замечаниях, как устроены деньги и что делать перед публикацией — в статье «Разработка, публикация и проверка Lovsel».
Обновление плагина
- Увеличьте
versionвMANIFEST(например,1.0.0→1.1.0). - Откройте плагин в кабинете разработчика → Загрузить сборку. Новая сборка сразу работает у вас, покупатели пользуются выпущенной версией.
- Проверьте изменения и нажмите «Выпустить», описав, что нового. Покупатели получат обновление автоматически, а сборка уйдёт на проверку Lovsel.
Lovsel хранит файлы только последней сборки и версии у покупателей: загруженная сборка заменяет предыдущую, а после выпуска файл прошлой версии удаляется. Во вкладке «Версии» остаётся история — номер обновления, дата и «что нового». Исходники храните у себя.
Совместимость данных:
- не переименовывайте ключи
ctx.storageи настроек без миграции; - при изменении формата храните версию данных и переводите старые значения при первом запуске:
@app.on_start
def migrate(ctx):
if ctx.storage.get("schema", 1) < 2:
slots = ctx.storage.get("slots", [])
for s in slots:
s.setdefault("region", "ar")
ctx.storage.set("slots", slots)
ctx.storage.set("schema", 2)Новое разрешение в манифесте (например, marketplace.api) покупатель увидит на карточке плагина после выпуска версии — описывайте в «Что нового», зачем оно понадобилось.
Совместимость со старыми плагинами
Плагины прежнего формата (register(cardinal), меню в Telegram) продолжают работать. В кабинете они открываются в совместимом режиме: экран плагина и его кнопки отображаются карточкой с плитками действий. Для публикации в каталоге рекомендуем перейти на WebPlugin:
| Было | Стало |
|---|---|
def register(cardinal) + cardinal.bus.register(...) | @app.on("new_order") |
settings(cardinal, chat_id) с кнопками | app.settings([...]) |
panel.register_callback(...) | @app.action("...") + ui.button(action=...) |
| Ожидание текстового ввода | ui.form(...) или ui.wizard(...) |
| Сообщения с прогрессом «14/50» | ctx.operations.start(...) |
panel.notify_admins(text) | ctx.notify(title, body) |
Свои JSON-файлы в configs/ | ctx.storage |
cardinal.api и внутренние клиенты площадок | готовые методы и ctx.ggsel.request, ctx.funpay.request, ctx.playerok.graphql |
Оба способа можно совмещать в одном файле: register(cardinal) для Telegram-меню и app = WebPlugin(...) для кабинета.
Полные примеры
- Самый короткий плагин (FunPay) — «Быстрый старт» в начале этого руководства.
- Связки товаров в духе KOSell (GGsel) — выбор двух товаров поиском или из каталога, мастер, список с переключателями и редактором, аренды по заказам, показатели и график, настройки с автосохранением. Пошаговый урок, файл
docs/examples/bundle_rent_example.py. - GGsel — «Благодарности покупателям»: меню слева, правила с мастером, шаблон с предпросмотром, история с поиском. Разбор, файл
docs/examples/ggsel_thanks.py. - FunPay — «Отзывы и напоминания»: сценарии во вкладках, заказы с этапами, отзывы с фильтрами, переписка. Разбор, файл
docs/examples/funpay_reviews.py. - Playerok — «Приветствие покупателям Playerok». Разбор, файл
docs/examples/playerok_greetings.py. - Короткие примеры площадок — отчёт по продажам для GGsel и благодарность за оплату для FunPay.
- Автозакупка за единый баланс (GGsel) — резерв под заказ, покупка у поставщика через
ctx.http, ожидание пополнения, выдача покупателю. Разбор, файлdocs/examples/supplier_purchase.py. - Всё ядро сразу — страницы, настройки, операция с прогрессом, файлы:
docs/examples/example_web_plugin.py.
Все примеры можно скачать кнопкой «Пример плагина» в документации.
Справочник
WebPlugin(manifest)—page(),action(),settings(),on(),on_start.ctx.ggsel,ctx.funpay,ctx.playerok— готовые методы площадки плагина и прямой доступ к её API:request()(GGsel, FunPay),graphql()(Playerok);ctx.marketplace— площадка плагина (одна).MarketplaceError— площадка отказала или не ответила (marketplace,status,code,message,body,unknown_result);MarketResponse— ответ наctx.funpay.request.ctx.http— запросы во внешние сервисы (network):get(),post(),request(),egress,route.ctx.wallet,WalletOperation,PurchaseResult,PurchaseFailed,WalletError— единый баланс;ctx.secrets— ключи сервисов на сервере («Единый баланс»).ui— блоки интерфейса,ui.field.*,ui.wizard(),ui.editor(),ui.settings(),ui.choices(),ui.result(),ui.dialog()— «Компоненты интерфейса SDK».ValidationError({field: text})— ошибка формы.PermissionDenied— не объявлено разрешение или площадка другая.UploadedFile— файл из формы.lovsel_sdk.testing.load_plugin(),plugin.page(),plugin.action(),plugin.submit(),plugin.emit(),plugin.api_calls,plugin.market_actions,check_blocks()— тестовый стенд.
Вопросы и предложения по SDK — через поддержку Lovsel.