К содержанию
Общее · Разработчикам

Как создать плагин для Lovsel

Всё, что разрешает API площадки: манифест и разрешения, готовые методы и прямой доступ к API, ctx.http, проверка и публикация.

Содержание

Плагин — дополнительные возможности площадки. Всё, что разрешает API площадки, можно сделать в плагине: Lovsel не сводит плагины к набору готовых функций. Если площадка позволяет действие, разработчик может реализовать его в своём плагине — готовым методом SDK или прямым запросом к API площадки через подключение магазина в Lovsel.

Это руководство для разработчика, который впервые видит Lovsel — кабинет продавцов на площадках GGsel, FunPay и Playerok: как устроен плагин, что даёт ядро, как обращаться к площадке, как построить интерфейс, который выглядит частью Lovsel, как проверить плагин и выпустить его в магазин.

Руководство общее для всех площадок. Отдельные статьи:

Главное правило интерфейса. Плагин описывает данные и действия, а Lovsel превращает их в веб-интерфейс: карточки, таблицы, формы, прогресс. Не стройте плагин вокруг Telegram-кнопок и текстовых меню.

Принцип: всё, что разрешает API площадки

Плагин работает от имени магазина продавца через подключение, которое продавец уже настроил в Lovsel. Доступ к площадке устроен в два уровня:

  1. Готовые методы — ctx.ggsel.orders(), ctx.funpay.send_message(), ctx.playerok.deliver() и другие. Они берут сохранённые данные Lovsel (быстро и без лишних запросов к площадке), учитывают особенности площадки и защищают от повторов: одна выдача на сделку, сверка действия, ответ которого потерялся.
  2. Прямой доступ к 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:

python
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 + "**")])]

Как попробовать:

  1. Сохраните код в файл hello.py.
  2. В Lovsel откройте Плагины → Разработчикам → Создать плагин, выберите файл и нажмите «Создать и тестировать у себя».
  3. Плагин сразу появится в меню кабинета и откроется на отдельной странице с вкладкой «Обзор». Модерация для этого не нужна.

Дальше — урок «Создаём плагин как KOSell: связки товаров»: по шагам от пустого файла до плагина с каталогом товаров, мастером, списком, статистикой и настройками. Другие полные примеры: «Благодарности покупателям» для GGsel и «Отзывы и напоминания» для FunPay. Пример со всеми возможностями ядра — настройками, операцией и файлом — docs/examples/example_web_plugin.py.

Как плагин живёт в Lovsel

  1. Загрузка. Lovsel читает MANIFEST без выполнения кода, проверяет версию, разрешения и площадку, затем импортирует файл. Ошибка импорта видна во вкладке «Разработка» кабинета разработчика и в карточке плагина.
  2. Запуск. Для каждого продавца создаётся свой контекст ctx, подписываются события @app.on, вызываются функции @app.on_start (миграция данных, фоновые задачи). Плагин только для FunPay или только для Playerok ждёт, пока продавец подключит площадку.
  3. Работа. Открыл продавец вкладку — вызывается функция страницы @app.page (до 15 секунд), нажал кнопку, отправил форму, переключил связку или ищет товар — вызывается действие @app.action (до 20 секунд). Пришёл заказ — вызывается обработчик события. Всё долгое — в операции ctx.operations.
  4. Выключение и обновление. Выключенный плагин не получает событий, его разделы закрыты, данные сохраняются. Новая версия заменяет код, а ctx.storage и настройки остаются — переводите старые данные в @app.on_start (см. «Обновление плагина»).
  5. Удаление. Продавец удалил плагин — файл плагина убирается из его кабинета, события и действия больше не вызываются.

Структура файла

python
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:

python
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».

Что даёт каждая площадка:

GGselFunPayPlayerok
Доступ из плагинаctx.ggselctx.funpayctx.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_idreply_review(), balance(), usernamereviews(), balance(), username
Прямой доступ к APIrequest() — Seller API v1 и v2request() — сайт FunPay от имени аккаунтаgraphql() — GraphQL сайта Playerok
Событияnew_order, order_status, new_chat, new_messagefunpay.new_order, funpay.order_status, funpay.new_chat, funpay.new_message, funpay.new_reviewplayerok.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 площадки».

ПлощадкаМетодЧто это
GGselctx.ggsel.request(method, path, params=, json=, version="v1", headers=)Любой метод GGsel Seller API. Список методов — официальная документация seller.ggsel.com/docs.
FunPayctx.funpay.request(method, path, params=, data=, json=, files=, ajax=False, csrf=True)У FunPay нет публичного API: запрос к сайту funpay.com через сессию аккаунта — всё, что продавец делает на сайте.
Playerokctx.playerok.graphql(query, variables, operation_name, persisted_hash=)Любой запрос и действие GraphQL сайта playerok.com от имени аккаунта.
python
# 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"
statusHTTP-код ответа или None, если ответа нет
codenetwork — нет связи, auth — площадка не приняла вход, blocked — запрос остановила защита сайта, rate_limited — слишком часто, http — ответ с ошибкой, graphql — ошибка запроса Playerok, bad_response — ответ не в ожидаемом формате, not_found — объект не найден, unknown_result — исход действия неизвестен; у отдельных действий — свои коды: price_changed, bad_state, foreign_url
messageТекст для продавца — его можно показать как есть
bodyНачало ответа площадки, если он был
python
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 отрисовывает их своими компонентами, поэтому интерфейс выглядит как встроенные разделы, поддерживает тёмную и светлую тему и телефоны.

python
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").

python
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) — в разделе «Встраивание в разделы кабинета».

Действия и кнопки

python
@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=)Варианты для поиска в поле выбора товаров.

Ошибку ввода покажите у поля:

python
from lovsel_sdk import ValidationError

if price <= 0:
    raise ValidationError({"price": "Цена должна быть больше нуля"})

Любое другое исключение Lovsel покажет пользователю как «Ошибка плагина: …» и запишет в журнал с трассировкой. Текст исключения должен быть понятен продавцу.

Формы и пользовательский ввод

python
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().

python
@app.action("add_slot")
def add_slot(ctx, data):
    keys = data.get("keys")
    lines = keys.lines() if keys else []
    ...

Не просите пользователя вводить данные по одному сообщению («введите название», «теперь введите цену»). Одна форма — одно действие.

Настройки

python
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 — свои настройки.

python
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.

Длительные операции и прогресс

Всё, что дольше пары секунд, оформляйте операцией. Пользователь видит, что происходит, сколько готово и сколько осталось, и может остановить операцию.

python
@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 с таблицей.
python
ctx.notify(
    "Поставщик не ответил",
    "Цены не обновлены 30 минут. Проверьте ключ API.",
    level="error",
    product="Steam Gift Card",
)

Ошибки страниц, действий и обработчиков событий Lovsel записывает в журнал сам — с трассировкой до строки вашего файла. Их видно в пункте «Журнал» меню плагина; ошибка загрузки плагина видна и во вкладке «Разработка» кабинета разработчика.

События площадок

Плагин подписывается на события декоратором @app.on(...). Первый аргумент обработчика — ctx, второй — словарь события:

python
@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 — магазин запущен—
GGselnew_order, order_status, new_chat, new_messageСобытия GGsel
FunPayfunpay.new_order, funpay.order_status, funpay.new_chat, funpay.new_message, funpay.new_reviewСобытия FunPay
Playerokplayerok.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. Выдача:

python
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.

python
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: продавцу не нужно регистрироваться у поставщика, хранить свой ключ и пополнять там счёт. Схема:

  1. Ключ поставщика — на сервере Lovsel. Lovsel заводит аккаунт у поставщика, а администратор сохраняет его API-ключ в карточке плагина («Админ-панель → Маркетплейс → плагин → Ключи сервисов»). Плагин читает ключ через ctx.secrets.require("SUPPLIER_API_KEY"); продавцы его не видят, в браузер он не попадает.
  2. Продавец платит со своего баланса Lovsel. Под оплаченный заказ плагин резервирует сумму через ctx.wallet (purchase() или reserve()), покупает у поставщика с общего аккаунта и после покупки списывает резерв — закупка плюс комиссия Lovsel. Не хватает денег — заказ ждёт пополнения, ошибка поставщика — резерв снимается. Подробно — «Единый баланс Lovsel в плагинах».
  3. Поставщик видит один IP-адрес. Все запросы к поставщику идут с сервера Lovsel, поэтому для поставщика это запросы с одного адреса — IP-адреса сервера Lovsel. Если у поставщика есть белый список IP, внесите в него этот адрес. Он показан в среде разработки официального плагина («Плагины → Разработчикам» → плагин), в панели «Расширенные настройки»; там же можно проверить, какой адрес видят внешние сервисы.
  4. Другой постоянный адрес — через прокси. Если поставщику нужен другой постоянный IP (например, он пускает только адреса своей страны), администратор задаёт исходящий прокси в той же панели «Расширенные настройки». Он хранится служебным ключом LOVSEL_HTTP_PROXY в «Ключах сервисов» плагина, в белый список поставщика вносится адрес прокси. Через прокси идут только запросы ctx.http — поэтому код плагина отправляет все запросы к поставщику через ctx.http, а не через requests напрямую.
python
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 учитывает эти правила, а покупатели ждут от плагина интерфейса, как у встроенных разделов.

  1. Вкладки по задачам. Типичный набор: «Обзор», рабочие страницы («Связки», «Правила», «Товары», «История»), «Настройки», в конце — «Инструкция». Lovsel сам добавляет «Журнал», «О плагине» и «Настройки» для app.settings. Своей боковой панели у плагина нет.
  2. Первая страница — обзор. Плитки ui.stats с линией динамики, график ui.chart, «Требуют внимания» и одно главное действие.
  3. Список объектов. Таблица с поиском и вкладками-фильтрами, переключатель «Включено» в строке (format="switch"), изменение в редакторе ui.editor, удаление в меню «⋯» с подтверждением. Создание сложной записи — мастер на скрытой странице.
  4. Пустые состояния. Если данных нет, объясните, что сделать: ui.empty("Слотов нет", "Нажмите «Создать слоты»", action=...).
  5. Одна основная кнопка на экран (variant="primary"), остальные — обычные; опасные действия — в меню «⋯» или ui.details.
  6. Подтверждение для всего, что меняет магазин, списывает деньги или удаляет данные — и для готовых методов, и для прямых запросов к API.
  7. Прогресс для любой работы дольше двух секунд.
  8. Таблицы вместо текстовых списков, формы и мастера вместо пошагового ввода, бейджи для статусов.
  9. Сообщения покупателю — с предпросмотром. Шаблоны редактируются полем ui.field.template: продавец видит сообщение глазами покупателя.
  10. Понятные тексты. Русский язык, без технических кодов, числа с единицами («15 %», «3 шт.»), суммы — через ui.money(value, currency) или колонку format="money". Никогда не пишите знак валюты вручную: у площадок и заказов разная валюта — магазин GGsel может работать в долларах.
  11. Без имитации Telegram. Не рисуйте меню из эмодзи-кнопок и не отправляйте экраны текстом.
  12. Секреты — только поля secret и ctx.secrets; не выводите ключи на страницах и в журнале.
  13. Бережно к площадкам. Не опрашивайте GGsel, FunPay и Playerok в цикле без паузы: заказы, диалоги и товары уже есть в orders(), chats(), lots() и items(). Прямые запросы — по действию продавца, по событию или в операции с паузами.
  14. Одна площадка. Плагин работает с одной площадкой: проверяйте connected перед обращением к ней и показывайте понятную подсказку, если она не подключена.

Проверка плагина

Без сервера. В SDK есть тестовый стенд. Он отрисовывает каждую страницу и проверяет блоки: опечатка в типе блока, кнопка без действия, повторяющийся ключ поля, выбор товаров или переключатель, который вызывает несуществующее действие, будут видны сразу.

bash
python -m lovsel_sdk.testing my_plugin.py              # манифест, навигация и отрисовка страниц
python -m lovsel_sdk.testing my_plugin.py --action export

В автотестах:

python
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:

python
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:

python
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:

python
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:

python
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 МБ). В коде ссылайтесь на них по имени файла:

python
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 сам подставит адрес картинки, поэтому она одинаково открывается у вас и у покупателей, на компьютере и на телефоне. Подробно — в разделе «Изображения» справочника компонентов.

Публикация

Модерация, чтобы начать, не нужна: плагин работает у вас сразу после загрузки. Когда он готов:

  1. Нажмите «Опубликовать» в кабинете разработчика — бесплатно или по своей цене.
  2. Плагин появится в магазине, а выпущенная сборка уйдёт на проверку Lovsel.
  3. После проверки на карточке появится отметка «Проверен Lovsel».

Покупатели видят статус проверки, автора и разрешения до покупки. Как проходит проверка, что будет при замечаниях, как устроены деньги и что делать перед публикацией — в статье «Разработка, публикация и проверка Lovsel».

Обновление плагина

  1. Увеличьте version в MANIFEST (например, 1.0.0 → 1.1.0).
  2. Откройте плагин в кабинете разработчика → Загрузить сборку. Новая сборка сразу работает у вас, покупатели пользуются выпущенной версией.
  3. Проверьте изменения и нажмите «Выпустить», описав, что нового. Покупатели получат обновление автоматически, а сборка уйдёт на проверку Lovsel.

Lovsel хранит файлы только последней сборки и версии у покупателей: загруженная сборка заменяет предыдущую, а после выпуска файл прошлой версии удаляется. Во вкладке «Версии» остаётся история — номер обновления, дата и «что нового». Исходники храните у себя.

Совместимость данных:

  • не переименовывайте ключи ctx.storage и настроек без миграции;
  • при изменении формата храните версию данных и переводите старые значения при первом запуске:
python
@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.