У каждого плагина 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_block | app.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)— каталог поставщика на тысячи товаров, остатки, курсы, журнал синхронизаций.
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.
@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 — форма в отдельной вкладке панели разработчика, одна на всех продавцов:
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 в плагинах»). Сумму закупки задаёт ваш код, наценку — тоже он:
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)), если покупки у поставщика точно не было, и вернуть деньги продавцу. Списать резерв за продавца нельзя: списывает только код плагина после настоящей покупки.
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— маршрут словами для журнала, без логина и пароля.
@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:
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.0python -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_settings | app.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.storage | ctx.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.