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

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

Структура плагина, манифест, страницы, действия, события, проверка и публикация.

Содержание

Это руководство для разработчика, который впервые видит Lovsel. Из него вы узнаете, как устроен плагин, какие возможности даёт ядро, как построить интерфейс, который выглядит частью Lovsel, как проверить плагин и выпустить его в магазин.

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

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

Что такое плагин

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

Плагин может:

  • показывать собственный раздел в кабинете Lovsel — со своим меню, таблицами, формами и мастерами;
  • выполнять действия по кнопкам и формам;
  • запускать длительные операции с живым прогрессом;
  • реагировать на события площадок: новый заказ, новое сообщение, смена статуса, отзыв;
  • читать и менять данные подключённых площадок — GGsel и FunPay (в пределах объявленных площадок и разрешений);
  • хранить настройки и собственные данные;
  • принимать файлы от пользователя и отдавать файлы на скачивание;
  • отправлять уведомления в центр событий Lovsel и в Telegram продавца.

Плагин работает на сервере Lovsel, пока у продавца активна подписка. Ключи площадок плагин не получает: запросы к GGsel и FunPay выполняет Lovsel.

Быстрый старт

Минимальный рабочий плагин для обеих площадок — 30 строк:

python
from lovsel_sdk import WebPlugin, ui

MANIFEST = {
    "id": "acme.hello",
    "name": "Привет, Lovsel",
    "version": "1.0.0",
    "author": "ACME",
    "description": "Показывает число заказов на подключённых площадках.",
    "icon": "👋",
    "category": "tools",
    "permissions": ["orders.read"],
    "marketplaces": ["ggsel", "funpay"],
}

app = WebPlugin(MANIFEST)


@app.page("overview", "Обзор", icon="layout-dashboard")
def overview(ctx, params):
    stats = []
    if ctx.ggsel.connected:
        stats.append(ui.stat("Заказов GGsel", len(ctx.ggsel.orders())))
    if ctx.funpay.connected:
        stats.append(ui.stat("Заказов FunPay", len(ctx.funpay.orders())))
    if not stats:
        return [ui.empty("Площадка не подключена", "Подключите GGsel или FunPay в разделе «Подключение».")]
    return [ui.stats(stats), ui.card("Приветствие", [ui.text("Здравствуйте! Магазин: **" + ctx.shop_title + "**")])]

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

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

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

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

python
from lovsel_sdk import WebPlugin, ui, ValidationError

MANIFEST = {...}                                   # 1. описание плагина
app = WebPlugin(MANIFEST, layout="sidebar")        # 2. объект плагина (обязательно с именем app)

app.settings([...])                                # 3. настройки (необязательно)

@app.page("overview", "Обзор", group="Работа")     # 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.
categoryнетОдна из категорий ниже.
permissionsдаРазрешения, которые нужны плагину. Пустой список — если плагин не трогает магазин.
marketplacesнетПлощадки плагина: ["ggsel"] (по умолчанию), ["funpay"] или ["ggsel", "funpay"]. См. «Площадки».
capabilitiesнетЧто умеет плагин (для карточки каталога).
homepageнетСсылка на сайт или поддержку (https://…).

Категории: automation (автоматизация), pricing (цены), delivery (выдача товаров), suppliers (поставщики), messages (сообщения), analytics (аналитика), tools (инструменты), other.

Разрешения:

РазрешениеЧто открывает
orders.readистория продаж и подробности заказа
chats.readпереписка с покупателем
chats.writeотправка сообщений покупателям
products.readсписок и карточки товаров
products.writeизменение цены, остатка, описания, публикация
reviews.readотзывы покупателей
balance.readбаланс продавца на площадке
storageсобственное хранилище (объявляйте, если используете ctx.storage)
networkзапросы во внешние сервисы
filesприём и выдача файлов
notificationsуведомления в центр событий

Разрешения показываются покупателю до покупки и сверяются с кодом при проверке Lovsel. Методы ctx.ggsel.* и ctx.funpay.* работают только с объявленными разрешениями — иначе выбрасывается PermissionDenied с понятным текстом. Разрешения общие для площадок: orders.read открывает и заказы GGsel, и заказы FunPay — если плагин объявил обе площадки.

Возможности (capabilities): web_ui, settings, background, events, operations, telegram. web_ui и settings Lovsel определяет сам.

Контекст ctx

Свойство или методНазначение
ctx.settingsСловарь значений настроек (с учётом значений по умолчанию).
ctx.save_settings(values)Сохранить настройки программно.
ctx.storageХранилище плагина: get(key, default), set(key, value), delete(key), all(), incr(key). Значения — любые JSON-данные до 1 МБ.
ctx.ggselДанные подключённого магазина GGsel — API для GGsel.
ctx.funpayДанные подключённого аккаунта FunPay — API для FunPay.
ctx.marketplacesПлощадки плагина: ["ggsel"], ["funpay"] или обе.
ctx.operationsДлительные операции: start(title, total), get(id), active().
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 отмечает такие обращения.

Хранилище изолировано: разные магазины не видят данные друг друга, а при обновлении версии плагина данные сохраняются.

Площадки

Lovsel работает с несколькими площадками: GGsel и FunPay. Каждый плагин объявляет, для какой площадки он сделан:

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"],
    "marketplaces": ["funpay"],          # ["ggsel"] — по умолчанию; ["ggsel", "funpay"] — обе площадки
}
  • Без поля marketplaces плагин считается плагином GGsel — старые плагины работают как раньше.
  • Площадку можно поменять и в карточке плагина: карточка каталога получит отметку площадки, а покупатели смогут отфильтровать каталог по ней.
  • Плагин только для FunPay у покупателя не запускается, пока FunPay не подключён: в кабинете он отмечен «ждёт подключения» и включается сам сразу после подключения аккаунта. Плагин для обеих площадок работает, если подключена любая из них — проверяйте ctx.ggsel.connected и ctx.funpay.connected.
  • Неизвестные площадки в marketplaces — ошибка манифеста: форма создания плагина покажет её сразу.
  • Плагин работает только со своими площадками. ctx.ggsel доступен плагинам с "ggsel" в marketplaces (иначе PermissionDenied), события GGsel (new_order, new_message…) приходят только им, а события funpay.* — только плагинам с "funpay". Событие init получают все.
  • В меню кабинета покупатель видит плагины выбранной площадки; в режиме «Все площадки» — все.
  • Интерфейс плагина окрашивается в цвет площадки: GGsel — зелёный, FunPay — голубой. Подробнее — в статье «Компоненты интерфейса SDK».

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

GGselFunPay
Доступ из плагинаctx.ggselctx.funpay
Заказыorders(), order(), currency()orders(), order()
Перепискаmessages(), send_message() — диалог заказаchats(), messages(), send_message() — диалог с покупателем
Товарыproducts(), product(), set_price(), update_product(), set_active()lots(), refresh_lots(), set_lot_price(), set_lot_active(), raise_lots()
Прочееreviews(), balance()balance(), username
Событияnew_order, order_status, new_chat, new_messagefunpay.new_order, funpay.order_status, funpay.new_chat, funpay.new_message, funpay.new_review
ПодробноПлагины для GGselПлагины для FunPay

Разрешения общие для площадок: orders.read открывает и заказы GGsel, и заказы FunPay — если плагин объявил обе площадки.

Страницы и навигация

У плагина свой раздел в кабинете с собственным меню. Страница — функция, которая возвращает список блоков. Lovsel отрисовывает их своими компонентами, поэтому интерфейс выглядит единообразно, поддерживает тёмную тему и телефоны.

python
app = WebPlugin(MANIFEST, layout="sidebar")        # меню слева внутри плагина


@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):
    ...
  • layout: "sidebar" — меню слева, "tabs" — вкладки сверху, "auto" — меню слева, если страниц больше трёх.
  • group — группа в меню, badge — счётчик, hidden=True — страница без пункта меню, bottom=True — пункт внизу меню.
  • Второй аргумент страницы — params: параметры из кнопки (ui.button(page=..., params=...)), строки таблицы (params["id"]), поиска и фильтров панели ui.toolbar, вкладок. Они хранятся в адресе страницы.
  • Функция страницы должна отработать быстро (до 15 секунд). Всё долгое — в операции.

Основные блоки:

БлокНазначение
ui.header, ui.card, ui.grid, ui.section, ui.tabs, ui.detailsШапка страницы и раскладка.
ui.stats, ui.chart, ui.keyvalue, ui.list_view, ui.timeline, ui.stepsПоказатели, графики, списки, ленты и этапы.
ui.table, ui.toolbar, ui.search, ui.filterТаблицы с поиском, фильтрами, выбором строк и меню действий.
ui.form, ui.wizard, ui.field.*Формы и пошаговые мастера.
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».

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

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) и загруженные файлы.
  • Действие должно завершиться за 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)Не перезагружать страницу.

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

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 → число, 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"].

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

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

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").
  • Проблема, требующая внимания продавца — 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

В каждом событии есть поле marketplace ("ggsel" или "funpay"). События площадки приходят только плагинам, объявившим эту площадку, и только при подключённом аккаунте.

Обработчик должен работать быстро. Долгую реакцию запускайте операцией или своим потоком из @app.on_start. Если вы запускаете поток, проверяйте флаг остановки и не держите блокировки.

Файлы

Приём — поле 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

Проверка Lovsel учитывает эти правила, а покупатели ждут от плагина интерфейса, как у встроенных разделов.

  1. Меню по задачам. Типичный набор: «Обзор», рабочие страницы («Правила», «Товары», «Заказы», «История»), внизу — «Инструкция». Lovsel сам добавляет «Настройки» (если они объявлены), «Журнал» и «О плагине».
  2. Первая страница — обзор. Состояние плагина, ключевые показатели, «Требуют внимания» и одно главное действие.
  3. Список → карточка записи. Таблица с поиском и фильтрами, запись открывается на скрытой странице с кнопкой «назад».
  4. Пустые состояния. Если данных нет, объясните, что сделать: ui.empty("Слотов нет", "Нажмите «Создать слоты»", action=...).
  5. Одна основная кнопка на экран (variant="primary"), остальные — обычные; опасные действия — в меню «⋯» или ui.details.
  6. Подтверждение для всего, что меняет магазин, списывает деньги или удаляет данные.
  7. Прогресс для любой работы дольше двух секунд.
  8. Таблицы вместо текстовых списков, формы и мастера вместо пошагового ввода, бейджи для статусов.
  9. Сообщения покупателю — с предпросмотром. Шаблоны редактируются полем ui.field.template: продавец видит сообщение глазами покупателя.
  10. Понятные тексты. Русский язык, без технических кодов, числа с единицами («15 %», «3 шт.»), суммы — через ui.money(value, currency) или колонку format="money". Никогда не пишите знак валюты вручную: у площадок и заказов разная валюта — магазин GGsel может работать в долларах.
  11. Без имитации Telegram. Не рисуйте меню из эмодзи-кнопок и не отправляйте экраны текстом.
  12. Секреты — только поля secret; не выводите ключи на страницах и в журнале.
  13. Бережно к площадкам. Не опрашивайте GGsel и FunPay в цикле без паузы: заказы, диалоги и лоты уже есть в orders(), chats() и lots().
  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"

Товары и сообщения 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": "Спасибо!"}]

В 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)

Совместимость со старыми плагинами

Плагины прежнего формата (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

Оба способа можно совмещать в одном файле: register(cardinal) для Telegram-меню и app = WebPlugin(...) для кабинета.

Полные примеры

  • Для обеих площадок — «Быстрый старт» в начале этого руководства.
  • GGsel — «Благодарности покупателям»: меню слева, правила с мастером, шаблон с предпросмотром, история с поиском. Разбор, файл docs/examples/ggsel_thanks.py.
  • FunPay — «Отзывы и напоминания»: сценарии во вкладках, заказы с этапами, отзывы с фильтрами, переписка. Разбор, файл docs/examples/funpay_reviews.py.
  • Короткие примеры площадок — отчёт по продажам для GGsel и благодарность за оплату для FunPay.
  • Всё ядро сразу — страницы, настройки, операция с прогрессом, файлы: docs/examples/example_web_plugin.py.

Все примеры можно скачать кнопкой «Пример плагина» в документации.

Справочник

  • WebPlugin(manifest, layout=) — page(), action(), settings(), on(), on_start.
  • ctx.ggsel, ctx.funpay — данные площадок; ctx.marketplaces — площадки плагина.
  • ui — блоки интерфейса, ui.field.*, ui.wizard(), ui.result(), ui.dialog() — «Компоненты интерфейса SDK».
  • ValidationError({field: text}) — ошибка формы.
  • PermissionDenied — не объявлено разрешение.
  • UploadedFile — файл из формы.
  • lovsel_sdk.testing.load_plugin(), check_blocks() — тестовый стенд.

Вопросы и предложения по SDK — через поддержку Lovsel.