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

Две панели плагина: для покупателя и для разработчика

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

Содержание

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

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

Возможности у всех разработчиков одинаковые. Официальные плагины Lovsel устроены так же, как плагины сторонних разработчиков, — отличается только владелец (их ведут администраторы Lovsel) и то, куда идут деньги. Ограничения — только реальные ограничения API площадок и правила безопасности Lovsel.

Общие правила создания плагина — в руководстве «Как создать плагин», оплата закупок — в статье «Единый баланс Lovsel в плагинах», все блоки интерфейса — в «Компонентах интерфейса SDK».

Как устроены две панели

Панель покупателяПанель разработчика
Кто видитпродавец, который установил плагинтолько владелец плагина (у плагинов Lovsel — администраторы)
Где«Плагины» → плагин«Плагины» → «Разработчикам» → плагин → «Панель разработчика»
Что в нейработа с плагином: его функции, данные и настройки продавцаобщее для всех покупателей: ключи, прокси, наценки, каталог, закупки, отчёты
Как объявитьapp.page, app.section, app.settings, app.action, app.embed_button, app.embed_blockapp.dev_page, app.dev_settings, app.dev_action, app.dev_button, app.dev_block
ctxмагазин продавца: ctx.ggsel, ctx.funpay, ctx.playerok, ctx.storage, ctx.walletплагин целиком: ctx.shared, ctx.secrets, ctx.purchases, ctx.installs() — без магазина продавца
Данныеу каждого продавца свои (ctx.storage, ctx.settings)одни на всех продавцов (ctx.shared, ctx.dev_settings, ctx.secrets)

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

Если плагин не объявил ни одного раздела, вкладка «Панель разработчика» пустая и подсказывает, с чего начать. Плагины, написанные до появления панели, работают без изменений.

Что общее для всех покупателей

Три хранилища одни на плагин каталога. Читают их обработчики у продавцов, а меняет в основном панель разработчика:

  • ctx.secrets — ключи. API-ключи поставщиков, токены сервисов, пароли прокси. Хранятся только на сервере Lovsel, в браузер не уходят ни продавцу, ни вам: в форме видно лишь, задан ключ или нет. Код читает их ctx.secrets.get(name) и ctx.secrets.require(name), панель разработчика — ещё и пишет (ctx.secrets.set(name, value), ctx.secrets.delete(name)).
  • ctx.dev_settings — настройки разработчика из формы app.dev_settings: наценка, адрес API, названия. Секретные поля формы попадают в ctx.secrets, а не сюда.
  • ctx.shared — общие данные. Значения по ключу (get, set, delete, all) и коллекции записей ctx.shared.records(name) — каталог поставщика на тысячи товаров, остатки, курсы, журнал синхронизаций.
python
items = ctx.shared.records("items")
items.sync({"steam-100": {"title": "Steam 100 ₽", "cost": 92.5, "stock": 14}})   # привести к каталогу поставщика
row = items.get("steam-100")                       # {"title": "Steam 100 ₽", "cost": 92.5, "stock": 14}
items.update("steam-100", markup_percent=15)       # дополнить запись полями
empty = items.find(where={"stock": 0}, limit=50)   # точное совпадение полей
total = items.count()

ctx.shared.set("last_sync", "2026-10-28T12:00:00+03:00")
Метод коллекцииЧто делает
get(key, default), key in recordsодна запись
put(key, value), put_many(dict), update(key, **fields)записать (значение — JSON до 256 КБ)
delete(key or keys), clear()удалить
list(prefix=, where=, order=, limit=, offset=)записи с ключом и временем: order — "key", "-updated"
find(...), items(...), keys(...), count(...)только значения, пары, ключи, количество
sync(dict, remove_missing=True)новые добавить, изменённые обновить, пропавшие удалить

Общие данные получает только плагин из каталога Lovsel — купленный, бесплатный или ваша копия разработчика. У файла, который продавец загрузил сам под тем же именем, общих данных и ключей нет: ctx.shared.available — False.

Раздел панели разработчика

Раздел — вкладка панели разработчика. Обработчик получает (ctx, params) и возвращает блоки ui, как обычная страница плагина. Кнопки и формы вызывают @app.dev_action.

python
@app.dev_page("catalog", "Каталог", icon="package", description="Товары поставщика и цены для продавцов.")
def catalog(ctx, params):
    rows = [dict(r["value"], id=r["key"]) for r in ctx.shared.records("items").list(limit=1000)]
    return [
        ui.stats([ui.stat("Товаров", len(rows)), ui.stat("Нет в наличии", sum(1 for r in rows if not r["stock"]))]),
        ui.table([ui.column("id", "ID", format="code"), ui.column("title", "Товар"),
                  ui.column("cost", "У поставщика", format="money", currency="RUB", align="right")],
                 rows, searchable=True, empty_text="Каталог пуст — нажмите «Обновить каталог»."),
    ]


app.dev_button("catalog", "Обновить каталог", "sync_catalog", icon="refresh")   # кнопка в шапке раздела


@app.dev_action("sync_catalog", timeout=60)
def sync_catalog(ctx, data):
    fresh = {str(i["id"]): {"title": i["title"], "cost": i["price"], "stock": i["stock"]}
             for i in supplier(ctx).items()}
    done = ctx.shared.records("items").sync(fresh)
    return ui.result("Каталог обновлён: %(total)d товаров, новых %(added)d" % done)
  • app.dev_page(id, title, icon=, description=, group=) — раздел; порядок объявления — порядок вкладок.
  • app.dev_action(id, confirm=, timeout=) — действие кнопки или формы, по умолчанию до 20 секунд. Долгую работу выносите в ctx.operations.start(...), как в панели покупателя.
  • app.dev_button(page, label, action, icon=, confirm=, variant=) — кнопка в шапке раздела.
  • app.dev_block(page, title=) — ещё один блок раздела: удобно, когда разные части плагина добавляют свои карточки в один раздел (например, баланс у каждого поставщика на «Обзоре»).

Все блоки ui работают так же, как в панели покупателя: таблицы с поиском и меню строки, формы и мастера, графики, окна, переключатели. Иконки разделов — из списка «Компоненты интерфейса SDK».

Настройки разработчика и секреты

app.dev_settings — форма в отдельной вкладке панели разработчика, одна на всех продавцов:

python
def check(ctx, values):
    if values["markup_percent"] > 300:
        raise ValidationError({"markup_percent": "Больше 300 % — проверьте наценку"})
    return values


app.dev_settings([
    ui.field.text("api_url", "Адрес API поставщика", default="https://api.supplier.example"),
    ui.field.text("SUPPLIER_API_KEY", "Ключ API поставщика", secret=True),
    ui.field.text("SUPPLIER_PROXY", "Прокси для поставщика", secret=True, placeholder="socks5://логин:пароль@host:port"),
    ui.field.number("markup_percent", "Наценка, %", min=0, max=500, default=10),
    ui.field.money("markup_min", "Минимальная наценка", default=0),
], title="Поставщик и наценка", validate=check)
  • Поле secret=True сохраняется в ключи плагина: у продавцов его читает ctx.secrets.require("SUPPLIER_API_KEY"). Пустое поле при сохранении — оставить прежний ключ, «-» — удалить ключ.
  • Остальные поля — ctx.dev_settings["markup_percent"]: и в панели разработчика, и у продавцов (только чтение).
  • validate(ctx, values) проверяет значения (raise ValidationError) или возвращает исправленные.
  • Из кода панели настройки меняет ctx.save_dev_settings(values).

Ключи можно хранить и без формы: ctx.secrets.set("PARTNER_TOKEN", token) в действии панели разработчика — например, после входа в кабинет поставщика по логину и паролю. ctx.secrets.hints() — список ключей без значений (••••1234) для показа в разделе.

Контекст панели разработчика

В обработчиках app.dev_page, app.dev_action, app.dev_block и validate из app.dev_settings ctx — контекст панели разработчика. Магазина продавца в нём нет: ctx.wallet, ctx.ggsel, ctx.funpay и ctx.playerok выбрасывают PermissionDenied.

Свойство или методНазначение
ctx.panel"developer" (у продавца — "buyer"): общий код может проверить, где он работает.
ctx.shared, ctx.storageОбщие данные плагина (в панели разработчика ctx.storage — то же, что ctx.shared).
ctx.secretsКлючи плагина: get, require, names, set, delete, hints.
ctx.dev_settings, ctx.save_dev_settings(values)Настройки app.dev_settings без секретов.
ctx.httpЗапросы во внешние сервисы (разрешение network), как у продавца.
ctx.set_http_proxy(url), ctx.http_proxyИсходящий прокси плагина для всех продавцов и его описание без пароля.
ctx.server_ip()IP-адрес сервера Lovsel для белого списка поставщика.
ctx.purchasesЗакупки всех продавцов через ctx.wallet с этим плагином: list, count, get, stats, daily, release, refund.
ctx.installs()Сколько продавцов пользуются плагином: {"total", "active"}.
ctx.catalog_id, ctx.marketplaceНомер плагина в каталоге и его площадка.
ctx.log, ctx.log_error, ctx.notify, ctx.download, ctx.operationsЖурнал, уведомление вам, файл на скачивание, долгие операции.

Деньги: наценка и доход разработчика

Закупка у поставщика оплачивается с баланса продавца в Lovsel через ctx.wallet — так же у всех плагинов («Единый баланс Lovsel в плагинах»). Сумму закупки задаёт ваш код, наценку — тоже он:

python
def price_for(ctx, item_id, cost):
    row = ctx.shared.records("items").get(item_id) or {}
    percent = row.get("markup_percent")                  # своя наценка товара
    if percent is None:
        percent = ctx.dev_settings["markup_percent"]     # иначе общая
    return round(cost + max(cost * percent / 100, ctx.dev_settings["markup_min"]), 2)


res = ctx.wallet.purchase(order_id, price_for(ctx, item_id, cost), cost=cost, item=item_id,
                          buy=buy, check=check, title=title, supplier="Поставщик")
  • amount (второй аргумент) — сколько платит продавец за закупку, уже с вашей наценкой. Сверху Lovsel добавляет свою комиссию (процент из настроек сервиса) — её считает сервер.
  • cost — необязательная себестоимость у поставщика: по ней ctx.purchases.stats() считает наценку, а продавец видит только итоговую сумму.
  • Куда идут деньги. После списания сумма закупки поступает владельцу плагина: разработчику — на его баланс Lovsel, строкой «Доход от закупок через плагин» (с него вы оплачиваете поставщика со своего счёта). Комиссия остаётся Lovsel. У плагинов Lovsel без разработчика вся сумма остаётся Lovsel.
  • Возврат продавцу (op.refund() у продавца или ctx.purchases.refund(op_id) в панели разработчика) забирает вашу долю с вашего баланса. Если доступных денег не хватает, возврат не проходит (WalletError, код owner_funds).
  • В панели разработчика можно снять резерв зависшей закупки (ctx.purchases.release(op_id)), если покупки у поставщика точно не было, и вернуть деньги продавцу. Списать резерв за продавца нельзя: списывает только код плагина после настоящей покупки.
python
stats = ctx.purchases.stats(30)
# {"count": 41, "sellers": 9, "amount": 5320.0, "fee": 106.4, "cost": 4810.0, "profit": 510.0,
#  "refunded": 0.0, "income": 5320.0, "open": {"waiting": 1, "reserved": 0, "processing": 0}}
rows = ctx.purchases.list(status="open", limit=50)   # продавец — без полной почты: an***@mail.ru

Поставщик с белым списком IP и прокси

Запросы плагина к внешним сервисам уходят через ctx.http с сервера Lovsel. Поставщику с белым списком IP нужен постоянный адрес:

  • ctx.server_ip() — адрес сервера Lovsel: покажите его в разделе панели, чтобы внести у поставщика;
  • ctx.set_http_proxy("socks5://логин:пароль@host:port") — исходящий прокси плагина: все запросы ctx.http у всех продавцов пойдут через него ("" — снова напрямую). Пароль хранится как ключ плагина и в браузер не уходит;
  • ctx.http.via(proxy) — свой маршрут для одного поставщика: например, прокси из секретного поля настроек;
  • ctx.http.egress_ip() — какой адрес видит внешний сервис (делает запрос — вызывайте по кнопке);
  • ctx.http.route — маршрут словами для журнала, без логина и пароля.
python
@app.dev_action("check_route", timeout=30)
def check_route(ctx, data):
    http = ctx.http.via(ctx.secrets.get("SUPPLIER_PROXY")) if ctx.secrets.get("SUPPLIER_PROXY") else ctx.http
    found = http.egress_ip()
    if not found["ok"]:
        return ui.result("Адрес не определён: " + found["error"], tone="danger", refresh=False)
    return ui.result("Поставщик видит адрес %s (%s)" % (found["ip"], found["route"]))

Подробно о ctx.http — в разделе «Запросы во внешние сервисы».

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

docs/examples/supplier_purchase.py — «Автозакупка у поставщика» с обеими панелями:

  • покупатель: «Связки» (товар GGsel ↔ товар поставщика, цена закупки для продавца), «Закупки», своя настройка — текст сообщения покупателю;
  • разработчик: «Обзор» (закупки и доход за 30 дней, график, IP-адрес и проверка поставщика), «Каталог» (синхронизация, своя наценка товара), «Закупки продавцов» (снять резерв, вернуть деньги), «Поставщик и наценка» (app.dev_settings с ключом API и прокси в секретных полях).

Скачать пример можно в разделе «Плагины → Разработчикам» и в документации одним файлом.

Проверка на стенде

Стенд SDK проверяет обе панели и деньги на настоящей логике Lovsel:

python
from lovsel_sdk.testing import load_plugin


class FakeSupplier:                                           # поставщик-заглушка вместо настоящего API
    def __init__(self, base_url, api_key, timeout=20.0): pass
    def price(self, item_id): return 100.0
    def items(self): return [{"id": "A1", "title": "Steam", "price": 100, "stock": 3}]
    def buy(self, item_id, client_order_id): return {"ref": "S-1", "goods": "CODE-1"}
    def find(self, client_order_id): return None


plugin = load_plugin("supplier_purchase.py", balance=500, orders=[{"id": "9001", "product_id": 101}],
                     products=[{"id": 101}], secrets={"SUPPLIER_API_KEY": "test-key"},
                     dev_settings={"markup_percent": 10},
                     records={"items": {"A1": {"title": "Steam", "cost": 100, "stock": 3}}})
plugin.module.SupplierClient = FakeSupplier
plugin.action("add_link", {"product": "101", "item_id": "A1", "title": "Steam", "cost": 100})
plugin.emit("new_order", {"order": {"id": "9001", "product_id": 101, "chat_id": 9001}})
assert plugin.wallet_operations()[0].amount == 110.0         # 100 ₽ + 10 % наценки
assert plugin.developer_balance()["total"] == 110.0          # сумма закупки — разработчику

blocks = plugin.dev_page("catalog")                           # раздел панели разработчика
plugin.dev_action("sync_catalog")                             # кнопка панели разработчика
plugin.save_dev_settings({"markup_percent": 15})              # форма app.dev_settings
assert plugin.dev.purchases.stats()["profit"] == 10.0

python -m lovsel_sdk.testing my_plugin.py отрисует все страницы и разделы панели разработчика и проверит, что у каждой кнопки есть действие. На стенде деньги разработчика приходят пользователю 2, developer=False — плагин без разработчика (сумма остаётся Lovsel).

Безопасность

  • Ключи — только в секретных полях и ctx.secrets. Не показывайте их в блоках, не пишите в журнал и не отдавайте в действиях: всё, что вернул обработчик, видит браузер.
  • Панель разработчика видит только владелец плагина; продавцы видят только панель покупателя.
  • Общие данные и ключи получает только плагин из каталога Lovsel, а не файл, загруженный продавцом.
  • Деньги — только через ctx.wallet: резерв под заказ продавца, списание после покупки. Прямые обращения к таблицам баланса проверка Lovsel отмечает как опасные.
  • Плагины сторонних разработчиков проходят проверку Lovsel; разрешения wallet и network покупатель видит до установки.

Переход с «Расширенных настроек»

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

Было (3.38.0)Стало
app.admin_page, app.admin_action, app.admin_settingsapp.dev_page, app.dev_action, app.dev_settings (прежние имена работают)
app.admin_button(section, …), app.admin_block(section)app.dev_button(page, …), app.dev_block(page)
ctx.official.settings, ctx.official.storagectx.dev_settings, ctx.shared (прежние имена работают)
ctx.official.items — каталог поставщикаctx.shared.records("items")
ctx.official.suppliers() — поставщики с ключами и проксисекретные поля app.dev_settings или ctx.secrets, ctx.http.via(proxy)
наценка во встроенном разделе «Наценка»наценка в коде: сумма ctx.wallet.purchase(order_id, amount, cost=...)
«Обзор» и «Закупки» с прибыльюctx.purchases.stats(), ctx.purchases.list() в своём разделе

Частые вопросы

Чем официальные плагины Lovsel отличаются от остальных?

Только владельцем. Возможности у всех плагинов одинаковые: две панели, общие данные, ключи, прокси, наценка и закупки через единый баланс. Официальные плагины ведут администраторы Lovsel, и деньги за закупки через них остаются Lovsel.

Увидят ли покупатели плагина ключи API из панели разработчика?

Нет. Ключи хранятся на сервере Lovsel и в браузер не передаются никому — ни продавцу, ни разработчику. Код плагина читает их через ctx.secrets; главное — не выводить их на страницах и в журнале.

Куда приходят деньги за закупки через плагин?

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

Можно ли сделать наценку на отдельные товары?

Да, наценку целиком считает код плагина: храните общую наценку в app.dev_settings, а свою наценку товара — в записи ctx.shared.records("items"), и передавайте итоговую сумму в ctx.wallet.purchase.