Cluster · разработчикам

Публикация в вебшопе

Вебшоп находится на сайте в разделе «Каталог модулей». Автор загружает пакет и описание версии. Пользователь выбирает версию, принимает права и устанавливает собственный экземпляр.

Первая публикация получает постоянный ID. Следующие версии сохраняются под тем же ID. Содержимое опубликованной версии не меняется, исправление оформляется новой версией.

Для разработки начни с руководства, для проверки пакета используй MODULE_TESTING.md.

Пакет

Минимальный пакет:

ping/
    main.py
    manifest.json

main.py должен находиться в корне распакованного пакета. Он объявляет класс Module. Локальные Python-файлы и каталоги можно приложить рядом.

Для ZIP-архива из каталога модуля:

python3 -m zipfile -c ping.zip main.py manifest.json
python3 -m zipfile -l ping.zip

В списке должны быть main.py и manifest.json, без лишнего уровня каталогов. Для GitHub ядро также поддерживает один общий верхний каталог, если непосредственно в нём находится main.py. Произвольную вложенность оно не ищет.

Не добавляй в пакет виртуальное окружение, .git, кеши, пароли, Telegram-сессии и свои рабочие базы. Зависимости Python перечисляются в requirements. Их можно получить из PyPI при сборке или приложить в wheels.

Символические ссылки, специальные файлы, выход через .., абсолютные пути, повторяющиеся имена и зашифрованные ZIP-записи не принимаются. Исходные пределы распакованного пакета: 256 MiB и 10000 файлов. Оператор может изменить их.

Размер публикации входит в квоту автора. Сборка установленного экземпляра отдельно занимает квоту его юзербота.

Описание версии

Для лучшего понимания полей:

from typing import Literal, NotRequired, TypedDict

class Rule(TypedDict):
    chats: NotRequired[list[int] | Literal["*"]]
    users: NotRequired[list[int] | Literal["*"]]
    domains: NotRequired[list[str] | Literal["*"]]
    modules: NotRequired[list[str] | Literal["*"]]

class Manifest(TypedDict):
    name: str
    description: str
    version: str
    permissions: dict[str, Rule]
    dependencies: list[str]
    changes: str
    interface: NotRequired[dict[str, object]]
    source: NotRequired[bool]
    license: NotRequired[str]
    compatibility: NotRequired[str]
    media: NotRequired[list[str]]
    requirements: NotRequired[list[str]]
    build: NotRequired[str]

Manifest и Rule здесь описывают JSON. Это не классы, которые нужно импортировать в модуле.

ПолеОбязательноеСодержимое
nameДаНазвание, до 128 байт UTF-8
descriptionДаНазначение и возможности, до 8192 байт
versionДаНапример, 1.0.0 или 1.1.0-beta.1
permissionsДаПрава и ограничения
dependenciesДаСписок ID других модулей, можно []
changesДаИзменения этой версии, строка до 8192 байт
interfaceНетКоманды, обработчики, поля и страницы
sourceНетПоказ исходников при сравнении версий
licenseНетОбозначение лицензии, до 512 байт
compatibilityНетСведения о совместимости, до 512 байт
mediaНетДо 8 HTTPS-адресов
requirementsНетДо 100 ограничений версий Python-пакетов
buildНетИмя собственного скрипта сборки без .py

Версия содержит три числовые части, от 1 до 5 цифр каждая. Дополнительная часть после дефиса допускает латинские буквы, цифры, точки и дефисы, до 32 символов.

compatibility является описанием. Эта строка сама не выбирает Python, образ или системные библиотеки. Реальную совместимость проверяют установкой в среду исполнения.

source=false скрывает исходники в сравнении версий. Это не шифрование пакета и не замена условиям лицензии.

Готовые описания: проверка связи и счётчик с кнопками. Формат прав находится в PERMISSIONS.md, интерфейса в MODULES.md.

Публикация через сайт

Профиль

Войди в профиль Cluster с подтверждённой почтой. Для установки и запуска своего экземпляра нужна активная подписка. Для команд подключи Telegram-аккаунт, а при использовании кнопок или Bot API личного бота.

Публикация относится к профилю автора. Дополнительная овнерка в чужом юзерботе не переносит на него авторство.

Архив

Открой «Каталог модулей» и нажми «Опубликовать модуль».

В блоке «Пакет модуля» выбери ZIP-файл в поле «Архив с компьютера». После этого заполняется описание.

Наличие manifest.json внутри архива не заполняет форму автоматически. Форма является отдельным источником описания, которое сохраняет ядро. Перенеси из файла значения полей и объект interface.

Поля формы

Поле сайтаЧто указать
«Название модуля»name
«Описание модуля»description
«Номер версии»version, например 1.0.0
«Изменения в этой версии»changes
«Зависимости от других модулей»Элементы dependencies
«Пакеты Python»Элементы requirements
«Описание команд, полей и страниц»Только объект interface
«Открыть исходники в сравнении версий»source
«Показывать модуль в общем каталоге»Публичность публикации
«Запрашиваемые права»Содержимое permissions через выбор прав

Открой «Описание команд, полей и страниц» и нажми «Вставить JSON». Вставь только объект interface, затем нажми «Применить значение». Весь manifest.json в это поле не помещается. Для примера с командами это такой объект:

{
    "commands": [
        {"id": "echo", "alias": "echo", "access": "owner", "source": "both"},
        {"id": "ping", "alias": "ping", "access": "owner", "source": "both"}
    ]
}

В блоке прав отметь telegram.read, telegram.send, bot.read и bot.send. Если автор заранее ограничивает чаты или другие ресурсы, задай их там же. Права из файла в архиве не принимаются автоматически.

Для счётчика перенеси его полный interface: команды, обработчик конфигурации, кнопки, действия, поля и страницы. Только списка команд ему недостаточно.

Поля license, compatibility, media и build поддерживаются API, но отдельных полей для них в текущей форме сайта нет. Для такой публикации передай полное описание через API. Не добавляй эти значения в interface: это другой объект.

Первая проверка

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

Нажми «Опубликовать версию модуля». После успеха появится сообщение «Версия опубликована» с ID публикации. Нажми «Открыть страницу модуля» и проверь название и выпуск. Для следующих версий используй управление публикацией, ID подставляется автоматически.

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

Установка

В карточке или на странице модуля нажми «Установить».

  1. Выбери нужную версию.
  2. Получи список запрашиваемых прав и зависимостей.
  3. Отметь разрешаемые права и ограничения.
  4. Задай название экземпляра, например main.
  5. При конфликте названий измени алиас команды.
  6. Нажми «Установить с выбранными правами».

После установки открой «Мои модули» и нажми «Запустить». Установка сама не означает, что обработчики уже работают.

Открой настройки экземпляра и заполни обязательные поля. Для проверки используй личный тестовый чат. Убедись, что модуль не отвечает постороннему при access="owner" и не получает доступ вне принятых ограничений.

Публикация проверяет архив и описание без исполнения main.py в ядре. Ошибка в Python-коде или несоответствие декораторов может проявиться только при сборке и запуске экземпляра. Успешная публикация не заменяет эту проверку.

Пакет по ссылке

Вместо локального файла заполни «Ссылка на архив или репозиторий GitHub» прямой HTTPS-ссылкой на ZIP-файл и нажми «Получить пакет по ссылке».

Адрес должен возвращать архив, а не страницу предпросмотра или форму входа. Адреса локальной сети и служебных интерфейсов сервера закрыты. Перенаправления автоматически не обходятся.

После получения пакета заполни описание так же, как для локального архива. Кнопка получения ещё не публикует версию.

У обычной ссылки сохраняется вид источника url. Не рассчитывай на постоянство содержимого внешнего адреса: опубликованный пакет уже сохранён в Cluster и не подменяется последующим изменением ссылки.

GitHub

В поле ссылки укажи адрес корня репозитория:

https://github.com/example/cluster-module

В поле «Ветка, тег или коммит GitHub» укажи нужную версию исходников. Можно использовать ветку, тег или полный SHA коммита. Пустое поле означает HEAD.

Не вставляй в это поле ссылку на /tree/... или /blob/.... Ссылка задаёт сам репозиторий, версия выбирается соседним полем.

При получении ядро определяет SHA коммита и скачивает архив именно этого коммита. В описание источника сохраняются репозиторий и SHA. Следующее изменение ветки не меняет уже опубликованный пакет.

В корне репозитория должен быть main.py. Если модуль лежит внутри большого проекта, подготовь отдельный ZIP с нужным корнем.

В текущем получении из GitHub нет поля для авторизации в закрытом репозитории. Для него подготовь архив локально и загрузи файлом. Переданные в URL пароли не являются способом подключения.

Ограничения GitHub API, недоступная ветка и отсутствие коммита приводят к ошибке получения. Публикация в таком случае ещё не создана.

Публикация через API

Для своего клиента используются маршруты сайта, а не операторский сокет. Нужен обычный сеанс автора, HTTPS и CSRF. Вход описан в API.md.

Последовательность:

ЗапросНазначение
POST /api/auth/loginВход автора
GET /api/auth/profileПолучение своего unit
POST /api/portal/request, действие files/createРезерв файла
Тот же маршрут, действие files/writeПоследовательная загрузка частей
Тот же маршрут, действие files/finishПроверка размера и SHA256
POST /api/catalog/publishПубликация готового файла и описания

В portal/request передаются id своего юзербота, action и объект data. Для files/create нужны filename, size и необязательный sha256.

files/write принимает id файла, offset и data в base64. Передавай части не больше 64 KiB после декодирования. Их смещения должны идти последовательно. Этот маршрут допускает увеличенное тело запроса для закодированной части.

В публикацию передаётся:

{
    "file": "FILE_ID",
    "module": null,
    "public": false,
    "manifest": {
        "name": "Первый модуль",
        "description": "Описание модуля.",
        "version": "1.0.0",
        "permissions": {},
        "dependencies": [],
        "changes": "Первый выпуск.",
        "source": true,
        "interface": {}
    }
}

Это пример структуры запроса. В нём нужно заменить FILE_ID и заполнить реальное описание. Пустой интерфейс не подходит модулю с командами.

file является ID готового файла Cluster, не путём на компьютере. module=null создаёт новую публикацию. Для обновления передай её существующий ID. public находится рядом с manifest, а не внутри него.

В полном manifest через API можно передать license, compatibility, media и build. Прочие неизвестные поля отклоняются.

Ответ имеет вид {"ok": true, "result": {...}}. В result возвращается запись версии с module, version, manifest, sha256 и сведениями об источнике. Внутренний путь сервера автору не передаётся.

Идентификаторы token, csrf, source, actor, profile не передаются полями JSON авторизованного запроса. API получает их из сеанса и заголовков. Не отключай проверку сертификата в клиенте.

Python-зависимости

dependencies и requirements имеют разный смысл.

Например, для meval добавь в описание:

{
    "requirements": ["meval==2.5"]
}

Это часть описания, остальные обязательные поля тоже нужны. На сайте внеси meval==2.5 в «Пакеты Python». Папка wheels для такого пакета необязательна.

Номер версии необязателен: "requirements": ["meval"] тоже принимается. При следующей сборке pip может выбрать другой совместимый выпуск.

Закрепляй версии, если следующая сборка должна использовать ту же версию пакета. Ограничения наподобие httpx>=0.28,<0.29 тоже принимаются, но разрешённая ими версия со временем может измениться. Зависимости самого пакета выбирает pip с учётом совместимости и ограничений. Конфликт версий останавливает сборку.

Если нужно приложить пакеты самостоятельно, используй структуру:

module/
    main.py
    manifest.json
    wheels/
        package_name-1.0.0-py3-none-any.whl

Подготовить пакет без двоичных расширений можно в совместимом окружении:

python3 -m pip download --only-binary=:all: --dest wheels "package-name==1.0.0"

Имя и версия здесь условные. Если приложенных файлов достаточно для всей установки, сборка проходит без обращения к PyPI. Если часть зависимостей отсутствует, pip использует и приложенные файлы, и PyPI.

Пакеты устанавливаются в .deps своего экземпляра. Общие пакеты ядра не меняются. Обычный перезапуск использует готовую сборку; повторная установка или новая сборка заново разрешает зависимости.

Среда сборки остаётся без прямой сети. Соединение pip через внутренний Unix-сокет передаёт запросы ядру отдельными операциями build/pypi/open и build/pypi/read. Они доступны только во время сборки при непустом requirements. Ядро допускает HTTPS-запросы к описаниям пакетов на pypi.org и файлам на files.pythonhosted.org, проверяет сертификаты и не следует перенаправлениям. Права web.request и web.full для установки не нужны и автоматически не выдаются.

Ссылки на сторонние хранилища, собственные индексы и параметры командной строки pip в requirements не принимаются. Сетевые команды в собственном скрипте сборки не получают прямой доступ в интернет. Поддержка PyPI относится к установке зависимостей из описания.

Скачанные файлы временные и удаляются после сборки, ошибки или отмены. Их общий размер ограничен резервом сборки, описание одного пакета ограничено 8 MiB, число получений ограничено 512. Готовые файлы входят в квоту юзербота. Если места недостаточно, сборка завершается ошибкой и освобождает резерв.

В поставляемом образе исполнения используется Python 3.12. Совместимость двоичных пакетов определяется Python и библиотеками среды модуля, а не версией Python процесса ядра. Пакет под CPython 3.13 не становится совместимым с 3.12 из-за запуска ядра на 3.13.

Для двоичных расширений учитывай архитектуру процессора, ABI Python, libc и внешние библиотеки. Проверь и среду сборки, и среду исполнения: установленная в образе сборки библиотека не обязательно есть в образе исполнения.

Если в PyPI есть только исходный архив, pip собирает его внутри среды сборки. Зависимости сборки также получаются через PyPI. Отсутствующий компилятор или системная библиотека требуют изменения сборочного образа оператором. Имя пакета в requirements не устанавливает системные пакеты через apt.

Собственная сборка

Для C-кода, библиотеки или дополнительных действий приложи Python-скрипт:

module/
    main.py
    manifest.json
    build.py
    hello.c

В описании версии:

{
    "build": "build"
}

Имя задаётся без .py. Это часть полного manifest, не interface. Для передачи этого поля используется API.

Макет build.py:

import subprocess
from pathlib import Path

Path("bin").mkdir(exist_ok=True)
subprocess.run(["cc", "hello.c", "-o", "bin/hello"], check=True)

Скрипт запускается в рабочей копии пакета после установки Python-зависимостей. Код сборки работает в gVisor с закрытой сетью. Наличие компилятора задаёт образ сборки, который настраивает оператор сервиса.

Не запускай apt, dnf или загрузку из интернета в build.py. Для дополнительных системных библиотек нужно заранее подготовленное окружение.

Результат проверяется по путям, типам файлов, размеру и числу записей. Файлы bin/ и библиотеки могут войти в результат. Ссылки и выход из рабочего каталога не принимаются.

Каталоги wheels после установки зависимостей не переносятся в итоговую сборку. Исходный опубликованный пакет остаётся неизменным.

Управление своими публикациями

В «Каталоге модулей» открой «Мои публикации». Здесь видны твои публичные и непубличные модули, а также публикации с ограничениями модератора. Кнопка с шестерёнкой на карточке и кнопка «Управление публикацией» на странице модуля открывают одни и те же настройки.

Удаление публикации не удаляет уже установленные у пользователей экземпляры. Их пакеты сохраняются и продолжают занимать хранилище автора, пока существует хотя бы один экземпляр этого модуля. После удаления последнего экземпляра ядро освобождает пакеты при обслуживании. Активная сборка также откладывает очистку. Картинки публикации и списки тестеров удаляются отдельно; ранее загруженный исходный ZIP остаётся обычным файлом автора, пока он сам его не удалит. Минимальная запись занятого ID сохраняется, чтобы удалённую публикацию нельзя было восстановить повторной отправкой старого номера.

Модуль, назначенный для автоматической установки новым юзерботам, удалить нельзя. Его назначенную версию также нельзя архивировать. Сначала оператор должен изменить настройку автоматической установки.

Тестеры

Непубличная публикация доступна автору и добавленным тестерам.

Открой свою публикацию, затем «Управление» и «Добавить тестера». Найди профиль по логину или имени и выбери его. Рядом с каждым добавленным тестером есть отдельная кнопка «Отозвать доступ».

Тестер должен быть зарегистрирован. Для установки нужен доступ к своему юзерботу и подписка. Он отдельно выбирает, показывать ли своё участие в карточке.

Отзыв доступа тестера убирает его доступ к непубличной публикации. Не считай эту кнопку командой удаления уже установленного кода: управление установленными экземплярами и модерация являются отдельными операциями.

Следующая версия

Для выпуска 1.1.0:

  1. Измени исходники и номер версии в локальном описании.
  2. Обнови changes, права и interface, если они изменились.
  3. Проверь новый запуск и перенос данных с предыдущей версии.
  4. Подготовь новый архив или коммит GitHub.
  5. Открой «Мои публикации», выбери модуль и в «Управлении» нажми «Добавить версию». ID подставляется автоматически.
  6. Опубликуй версию и проверь её в списке версий.

Пустое поле ID создаст другой модуль. Переименование отображаемого названия не объединяет такие публикации.

Версия с тем же ID и номером не перезаписывается другим содержимым. Совпадение пакета и описания позволяет вернуть прежнюю запись. Отличающийся пакет или описание под тем же номером отклоняется.

У пользователя есть три режима обновления: уведомление, автоматическое без новых прав, автоматическое с принятием всех новых прав. Последний выбирается явно.

В «Моих модулях» пользователь открывает «Обновления, права и управление», выбирает версию и нажимает «Проверить изменения и права». После проверки выбирает права и создание резервной копии.

По умолчанию перед обновлением сохраняется одна копия данных экземпляра. Новая копия заменяет предыдущую и занимает общую квоту. Ошибка обновления отключает экземпляр. Автоматического возврата старого формата данных нет.

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

Публичность и архивирование

Для вывода непубличного модуля в общий каталог открой «Управление», выбери «Всем пользователям каталога» и сохрани. Выпускать новую версию не требуется.

Архивирование относится к конкретной версии. Во вкладке «Управление» разверни нужную строку в списке «Версии». Для каждого выпуска есть архивация и возврат из архива. Через API catalog/archive передаются нужный номер и archived=false для возврата.

Архивированная версия недоступна для новой установки. Архивирование не является остановкой уже работающих экземпляров. Срочное прекращение работы опасной версии выполняется через модерацию сервиса.

Проверка и отзывы

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

Галочка относится только к проверенной версии. Она означает соответствие функционала описанию по результатам проверки администратора. Следующая версия её не наследует.

Отзыв может оставить профиль, у которого модуль установлен в собственном юзерботе. Оценка от 1 до 5, текст до 4096 байт UTF-8. Повторный отзыв того же профиля заменяет предыдущий.

Администратор может снять галочку, скрыть публикацию, запретить установки, остановить экземпляры или удалить их с уведомлением пользователя.

Продажа модулей пока не реализована. Скрытые исходники поддерживаются независимо от продажи.

Ошибки публикации

СитуацияПроверка
Не выбран пакетВыбери файл или сначала получи пакет по ссылке
«The description is incomplete»Заполнены все шесть обязательных полей
«main.py is missing»Правильный корень архива
«Published versions cannot be changed»Новый номер версии при любом исправлении
«This publication belongs to another author»Правильный ID и профиль автора
«The GitHub revision is unavailable»Репозиторий открыт, ветка или коммит существуют
«Declaration does not match the installed version»Совпадают код, интерфейс и порядок списков
«The build failed»Есть совместимые пакеты, скрипт и нужные библиотеки
Конфликт командыВыбран свободный алиас
Ошибка квотыЕсть место на архив, распаковку и сборку

После успешного выпуска сохрани исходники, описание, номер и SHA256 пакета. Для GitHub сохрани также коммит. Это позволит воспроизвести именно опубликованную версию.

Страница модуля и оформление

Нажми «О модуле» в каталоге. На странице находятся описание, сведения об авторе, права последней версии, выпуски с описанием изменений, исходники, сравнение и отзывы. Название автора открывает его профиль. Выпуски показываются отдельно, с датами и состоянием проверки, архива и блокировки.

«Исходники и сравнение» показывает только версии с source=true. В режиме файлов можно выбрать выпуск и открыть его файл. В сравнении выбираются две версии либо пустой пакет для первого выпуска. Добавленные и удалённые строки выделены разными цветами. Ограничения предпросмотра описаны в MODERATION.md. Администратор сервиса может проверять пакет и при закрытых исходниках.

В «Управлении» автору доступен раздел «Оформление». Здесь можно поставить или убрать аватарку и баннер. Сайт переводит PNG, JPEG или WebP в обычный PNG, ядро проверяет результат и удаляет метаданные. Картинки сохраняются отдельно от пакета и не требуют новой версии. Их объём входит в личную квоту автора; замена и удаление освобождают прежнее место. Размеры и порядок учёта описаны в PROFILES.md.

После публикации кнопка «Открыть страницу модуля» ведёт к этой странице. Оформление можно добавить сразу после первого выпуска. ID публикации находится в сведениях модуля и в дополнительных действиях вкладки «Отзывы».