Коротко
Плагин — папка в plugins/ с двумя файлами: plugin.json описывает плагин, в main.py лежит класс-наследник Plugin. Каждый включённый плагин ядро запускает отдельным процессом и обращается к нему само: передаёт фразы, спрашивает вкладку и плитку, передаёт нажатия.
С компьютером плагин работает через объявления. Инструменты с разрешениями уходят в реестр ядра, и там их проверяют так же, как встроенные: разрешения, подтверждение, запись в журнал. Разрешения перечислены в манифесте до первого запуска. Вкладка и плитка — данные, их рисует оболочка, поэтому в плагине нет ни строчки кода интерфейса.
Внутри своего процесса плагин остаётся обычным кодом на Python с правами пользователя. Где проходит граница, сказано в разделе «Граница».
| Версия API | 4; ядро загружает версии от 4 до 4 |
| Схема вкладки | версия 2 |
| Python | встроенный 3.12 ядра, без pip |
| Срок ответа | 10 с на вызов, 20 с на запуск |
Первый плагин
Самый маленький плагин из поставки отвечает на «привет». Вот он целиком: plugins/greeter/.
plugin.json
{
"id": "greeter",
"name": "Приветствие",
"version": "1.0.0",
"author": "NeuroSync Foundry Team",
"description": "Отвечает на приветствия. Скажите «привет» — и Рина ответит.",
"icon": "👋",
"entry": "GreeterPlugin",
"api_version": 4
}main.py
"""
A greeting.
An example of the **smallest** plugin: no tools, no page, no permissions —
only phrase parsing. And so it should be: declaring a tool for the sake of
"hello" is pointless, and the API requires declaring nothing.
"""
from plugins.api import Plugin
class GreeterPlugin(Plugin):
"""A simple greeting plugin — a demonstration of on_command."""
def on_enable(self):
self.log("Плагин приветствия готов")
def on_command(self, text):
low = text.lower()
if any(w in low for w in ("привет", "здравствуй", "хай", "hello")):
name = self.ctx.get_setting("user_name", "друг")
self.respond(f"Привет, {name}! Чем могу помочь?")
return True
if "пока" in low or "до свидания" in low:
self.respond("До встречи! 🌸")
return True
return False- Положите папку в
plugins/рядом с программой (в исходниках это корень репозитория) или поставьте папку или архив.zipиз раздела плагинов в программе. - Включите плагин там же. Код запускается при включении, до этого ядро читает только манифест.
- Скажите или напишите Рине «привет».
Если плагин не включается, причина написана в списке плагинов рядом с ним, а подробности — в его журнале.
Манифест
plugin.json в кодировке UTF-8. Ядро читает его без запуска кода, поэтому список плагинов виден до включения.
| Поле | Обязательно | По умолчанию | Что это |
|---|---|---|---|
id | да | — | Имя плагина. Настоящим именем служит имя папки: при установке папка называется по этому полю, а при загрузке ядро берёт имя папки, что бы ни стояло здесь. Отсюда же префикс инструментов plugin.<id>.. |
name | да | — | Как плагин называется в списке плагинов. |
version | нет | "1.0.0" | Версия вашего плагина, для пользователя. Ядро её не сравнивает. |
author | нет | "unknown" | Кто написал. |
description | нет | "" | Одна-две строки в списке плагинов: что он делает и как его позвать. |
entry | нет | "" | Имя класса в main.py. Пусто — ядро возьмёт первый найденный класс-наследник Plugin; с именем надёжнее. |
icon | нет | "🧩" | Значок в списке и на вкладке: символ или эмодзи. |
api_version | нет | 1 | Версия API, под которую написан плагин. Не указали — считается 1, и такой плагин не загрузится. |
permissions | нет | [] | Что плагину нужно от компьютера: имена из каталога разрешений. Инструмент может просить только то, что перечислено здесь. |
Имя папки при установке проверяется: оно должно подходить под ^[A-Za-z0-9_][A-Za-z0-9._-]*$ и не быть именем устройства Windows (con, prn, aux, nul, com1…com9, lpt1…lpt9, в том числе с расширением). Если собираетесь импортировать свои модули (см. ниже), берите имя, которое годится в Python: латиница, цифры, подчёркивание.
Манифест примера из раздела «Пример целиком»:
{
"id": "shopping",
"name": "Покупки",
"version": "1.0.0",
"author": "Вы",
"description": "Список покупок: голосом, на своей вкладке и плиткой на главном экране.",
"icon": "🛒",
"entry": "ShoppingPlugin",
"api_version": 4,
"permissions": []
}
Класс плагина
Класс наследует plugins.api.Plugin и переопределяет нужные хуки; остальные можно не трогать. Конструктор получает контекст. Если переопределяете __init__, передайте контекст дальше: super().__init__(context). После него доступны self.ctx и self.manifest.
Хуки
| Хук | Когда зовут | Что вернуть |
|---|---|---|
on_enable() | Плагин включили: процесс поднят, плагин представился ядру. | Ничего. Здесь заводят своё: поток, кэш, первый запрос. |
on_disable() | Плагин выключают в программе. При выходе из Рины этот хук не зовётся: процесс получает plugin.shutdown и завершается. | Ничего. Закройте то, что завели. |
on_command(text: str) -> bool | Пришла фраза. Плагины слышат её раньше встроенных команд и своих команд пользователя, по очереди в алфавитном порядке id. | True, если фраза ваша: дальше её никто не разбирает. False — фраза идёт дальше. |
on_event(name: str, data: dict = None) | Сейчас не зовётся: ядро 4.0 не рассылает плагинам событий. Хук оставлен для будущих версий. | Ничего. |
tools() | Один раз при запуске плагина: описание инструментов уходит ядру вместе с приветствием. Потом при каждом вызове инструмента, чтобы найти его run. | Список PluginTool. Держите метод быстрым и без побочных действий. |
page() | Пользователь открыл вкладку плагина, и после каждого on_action. | Список элементов страницы. Вкладка появляется, если метод переопределён. |
home() | Оболочка рисует главный экран. | Список элементов плитки. Пусто или None — плитки нет. |
on_action(action: str, value=None) | Нажата кнопка или отправлено поле на вашей вкладке или плитке. | Ничего. После вызова ядро само спросит page() заново. |
settings_schema() | Её читает self.setting(), чтобы найти значение по умолчанию. | Список полей из plugins.settings_spec. |
Ошибка внутри хука плагин не роняет: ядро получает ответ с ошибкой, а трассировка попадает в журнал плагина.
Поля класса
| Поле | По умолчанию | Что задаёт |
|---|---|---|
page_title | — | Заголовок вкладки. Не задан — имя из манифеста. |
page_icon | — | Значок вкладки. Не задан — значок из манифеста. |
Удобные методы
| Метод | Что делает |
|---|---|
log(message) | То же, что self.ctx.log. |
respond(text) | То же, что self.ctx.respond. |
setting(key, default=None) | Значение настройки: сохранённое, иначе умолчание из settings_schema(), иначе default. |
Как ядро зовёт плагин
Каждый включённый плагин — отдельный процесс python -u plugins/host.py <папка> на интерпретаторе ядра. Разговор идёт по stdin и stdout кадрами того же формата, что у оболочки с ядром (см. «Провод»).
| Этап | Что происходит | Срок |
|---|---|---|
| Обнаружение | Ядро читает манифесты. Код не запускается. | — |
| Включение | Поднимается процесс, плагин отвечает на plugin.hello, потом зовётся on_enable. | 20 с на приветствие |
| Работа | Фразы, вкладка, плитка, нажатия, инструменты. | 10 с на каждый вызов |
| Выключение | on_disable, затем plugin.shutdown и завершение процесса. | 3 с и 2 с |
| Выход из Рины | Только plugin.shutdown. | 2 с |
Не ответил в срок — остановлен. Процесс убивают, плагин выключается, а в списке плагинов появляется причина. Сосед при этом продолжает работать. Упавший процесс ядро тоже замечает и показывает плагин сломанным.
Фразы
Фраза приходит к плагинам раньше, чем к своим командам пользователя, встроенным действиям и языковой модели. Ядро спрашивает включённые плагины по очереди в алфавитном порядке id; первый, кто вернул True, забирает фразу. Отсюда два правила:
- Берите только своё. Плагин, который откликается на любое «открой», перебьёт встроенный запуск программ. Узкий набор начальных слов, как у
convert(«переведи», «пересчитай»), безопаснее поиска слова внутри фразы. - Отвечайте быстро. Каждая фраза — поход в ваш процесс, и пока плагин думает, Рина молчит. Решение «моя или нет» должно занимать миллисекунды.
Вернули True и ничего не сказали — Рина промолчит. Ответ произносит self.respond(...).
Потоки
Запросы ядра выполняются в одном рабочем потоке, по одному: пока идёт долгий вызов, следующие ждут в очереди, и срок у них общий. Долгую работу (сеть, диск) делайте в своём потоке и отвечайте из памяти, как rates. Из своего потока можно звать ctx.respond, ctx.log, ctx.notify и настройки: отправка в процессе защищена замком.
Вывод и журнал
stdout процесса занят проводом, поэтому хост перенаправляет print в stderr. Всё, что плагин пишет в stderr, попадает в журнал плагина, последние 100 строк. В журнал Рины этот вывод не идёт. Для сообщений о работе есть ctx.log.
Состояние
Поля экземпляра живут, пока жив процесс. Выключение, сбой или перезапуск Рины их стирают. Всё, что должно остаться, храните в настройках.
Контекст: self.ctx
Через контекст плагин обращается к ядру. Других дверей нет: плагин не видит соседей, окно программы и реестр.
| Метод | Что делает | Как идёт по проводу |
|---|---|---|
log(message: str) | Строка в журнал плагина. Её видно на странице плагина в программе. | событие plugin.log, без ожидания |
respond(text: str) | Рина скажет и покажет этот текст. | событие plugin.respond, без ожидания |
get_setting(key: str, default=None) | Ваша настройка по ключу, или default. | запрос plugin.setting.get; ждёт ответа до 5 с |
set_setting(key: str, value) | Записать настройку. Значение должно переживать JSON: строки, числа, bool, списки, словари. | событие plugin.setting.set, без ожидания |
notify(title, message) | Уведомление из трея: заголовок и текст. | событие plugin.notify, без ожидания |
get_setting — единственный вызов, который ждёт ответа. Если ядро не ответило за 5 с, вернётся последнее значение, которое плагин читал или писал сам, иначе default.
Инструменты
Инструмент — действие, которое ядро выполняет от имени плагина. Плагин описывает его и ждёт вызова; когда звать, решает ядро.
В 4.0 инструменты плагинов заводятся, но вызвать их пока нечем. Встроенные команды ведут к встроенным инструментам, у своих команд пользователя нет вида «инструмент», в протоколе нет метода для вызова по имени, а языковая модель реестр не видит. Инструменты объявляют заранее: так их права и подтверждения проверены уже сейчас, а звать их будет модель в следующих версиях. Чтобы плагин работал голосом сегодня, отвечайте на фразу в on_command, как все плагины из поставки.
Значение по умолчанию до run не доходит. Ядро пересобирает Param на своей стороне без поля default, поэтому необязательный аргумент может просто не прийти. Берите значение сами: args.get("sides") or 6, как в dice.
PluginTool
| Поле | Обязательно | По умолчанию | Что это |
|---|---|---|---|
name | да | — | Короткое имя. В реестре ядра инструмент живёт как plugin.<id>.<name>. |
summary | да | — | Одна строка: что инструмент делает. |
run | нет | — | Функция run(args), её вызывает ядро. args — словарь уже проверенных аргументов. |
params | нет | [] | Аргументы: кортеж Param из core.tools. |
permissions | нет | [] | Какие разрешения нужны. Каждое должно быть в манифесте. |
confirm_required | нет | false | Спрашивать подтверждение перед каждым вызовом. |
Param
Те же аргументы, что у встроенных инструментов. Реестр проверяет их до вызова: лишний аргумент, неверный тип, число вне границ или значение не из choices до плагина не дойдут.
| Поле | Обязательно | По умолчанию | Что это |
|---|---|---|---|
name | да | — | Имя аргумента, ключ в args. |
type | да | — | Тип: string, integer, number, boolean, array, object. |
description | да | — | Что это за аргумент. Обязательное поле: без описания Param не создаётся. |
required | нет | true | Обязателен ли аргумент. |
choices | нет | [] | Допустимые значения. Пусто — любые. |
minimum | нет | — | Нижняя граница для чисел. |
maximum | нет | — | Верхняя граница для чисел. |
default | нет | — | Значение, если аргумент не пришёл. |
Вызов и ответ
run(args)возвращает текст ответа. Через процесс уходит строка: любое значение ядро приведёт кstr,Noneстанет пустой строкой.- Исключение внутри
runпревращается в неудачу инструмента с кодомinternal, трассировка — в журнал плагина. confirm_required=True— ядро спросит пользователя перед каждым вызовом. Для опасного разрешения это обязательно: без подтверждения такой инструмент не создаётся.- Список инструментов ядро узнаёт при запуске. Изменили
tools()— выключите и включите плагин.
Разрешения
Каталог разрешений один на ядро и плагины (core/permissions.py). Плагин перечисляет нужное в манифесте, а инструмент — в своём поле permissions.
| Имя | Название | Что разрешает | Опасное | Плагину |
|---|---|---|---|---|
process.launch | Запуск программ | Открывать приложения и папки, которые вы называете. | нет | да |
system.media | Громкость и воспроизведение | Менять громкость и управлять плеером. | нет | да |
system.lock | Блокировка экрана | Блокировать рабочий стол по команде. | нет | да |
system.power | Выключение компьютера | Выключать, перезагружать и усыплять компьютер. Каждое такое действие подтверждается отдельно. | да | никогда |
screen.capture | Снимок экрана | Делать снимок экрана и сохранять его в «Изображения». | нет | да |
network.local | Обращение к локальным службам | Спрашивать языковую модель, работающую на этом компьютере. | нет | да |
network.external | Выход в интернет | Открывать поиск в браузере и обращаться к службам вне этого компьютера. | нет | да |
files.read | Чтение файлов | Читать файлы, которые вы указали. | нет | да |
files.write | Запись файлов | Создавать и изменять файлы. | да | никогда |
screen.read | Чтение содержимого экрана | Видеть, что происходит на экране. Понадобится, когда Рина научится работать с компьютером. | да | никогда (заведено к 5.0) |
input.synthesize | Управление мышью и клавиатурой | Нажимать и печатать от вашего имени. Понадобится, когда Рина научится работать с компьютером. | да | никогда (заведено к 5.0) |
- То, что плагину не выдаётся, и имена не из каталога ядро отбрасывает и пишет в журнал плагина: «Не выдано разрешений: …».
- Инструмент, которому нужно не выданное или не перечисленное в манифесте, не заводится вовсе. В журнале будет «Инструмент «имя» не заведён: просит […]».
- Опасное разрешение требует
confirm_required=True.
Разрешения описывают инструменты. Код плагина в своём процессе может обратиться к сети и без network.external; об этом раздел «Граница».
Вкладка
page() описывает вкладку плагина списком элементов из plugins.page_spec. Рисует их оболочка, какая бы она ни была. Вкладка появляется, если класс переопределил page(); заголовок и значок берутся из page_title и page_icon.
Схема смысловая. В ней нет полей о цвете, отступах и шрифте: плагин говорит «предупреждение» (Badge(..., "warn")), а как его показать, решает оболочка.
Элементы
| Функция | Что это и что уходит оболочке |
|---|---|
Title(text)вид title | Заголовок раздела.{"kind": "title", "text": "Покупки"} |
Text(text)вид text | Обычный текст.{"kind": "text", "text": "Молоко, хлеб"} |
Note(text)вид note | Пояснение мельче и тише.{"kind": "note", "text": "Обновлено в 9:40"} |
Items(items)вид items | Список строк.{"kind": "items", "items": ["молоко", "хлеб"]} |
Button(label, action, variant='normal')вид button | Кнопка. Нажатие приходит в on_action(action); variant: normal или danger.{"kind": "button", "text": "Очистить", "action": "clear", "variant": "danger"} |
Input(action, placeholder='', value='', button='')вид input | Поле ввода с кнопкой. Отправка приходит в on_action(action, введённый_текст); button — подпись кнопки.{"kind": "input", "text": "Что купить", "action": "add", "variant": "Добавить", "value": ""} |
Table(rows, headers=None)вид table | Таблица: строки из ячеек и, если нужно, заголовки.{"kind": "table", "items": [["USD", "1"]], "value": ["Код", "Курс"]} |
Progress(value, text='')вид progress | Полоса выполнения, value от 0 до 1; лишнее обрезается.{"kind": "progress", "text": "Скачано 40 %", "value": 0.4} |
Badge(text, variant='normal')вид badge | Короткая метка состояния: normal, good, warn, danger.{"kind": "badge", "text": "Готово", "variant": "good"} |
Divider()вид divider | Разделитель.{"kind": "divider"} |
Card(children, title='')вид card | Одна вещь целиком: заметка, результат, прибор. Заголовок необязателен.{"kind": "card", "text": "Список"} |
Group(children, title='')вид group | Раздел страницы на одну тему: заголовок и то, что под ним.{"kind": "group", "text": "Сегодня"} |
Row(children)вид row | Элементы рядом. На узком окне оболочка вправе поставить их столбиком.{"kind": "row"} |
Правила
- Вкладка обновляется целиком: после каждого
on_actionядро снова зовётpage(). Частичных обновлений нет, описывайте текущее состояние. - Вложенность — не глубже 4 уровней, глубже оболочка обрежет с пометкой.
rowвrowне кладут.- Пустой контейнер не рисуется.
- Незнакомый вид оболочка показывает заметно, с его именем: так плагин под более новую схему не теряет часть вкладки молча.
- Всё описание вкладки уходит одним сообщением, а сообщение больше 1 МиБ ядро не примет и остановит плагин. Длинные списки режьте.
Полное описание схемы — PAGE-SCHEMA-v2.
Плитка на главном экране
home() описывает плитку на главном экране теми же элементами, что и вкладку. Главный экран общий для всех плагинов, поэтому пустой ответ правилен для большинства из них.
- На плитку попадают первые 4 элемента. Остальное обрезается, и оболочка знает, что плитка обрезана.
- Плитку рисуют часто: всякий раз, когда пользователь приходит на главный экран. Собирайте её из того, что уже знаете. В сеть ходите по своему расписанию, в потоке.
- На плитке могут быть поле и кнопки: их нажатия приходят в
on_action, а плитка перерисовывается, когда оболочка снова рисует главный экран. Так сделанconvert. - Есть ли у плагина плитка, ядро узнаёт один раз, при приветствии: по тому, переопределён ли
home().
Настройки
Настройки плагина хранит ядро, в общем файле настроек Рины, под ключом plugin_settings → id плагина. Поэтому они переживают сбой и перезапуск процесса, а плагину не нужно знать, где лежит файл.
self.ctx.get_setting(key, default)иself.ctx.set_setting(key, value)читают и пишут одно значение.self.setting(key, default)вернёт сохранённое, иначе умолчание изsettings_schema(), иначеdefault.- Значения проходят через JSON: кортеж вернётся списком, а объект не сохранится.
Панель настроек по схеме в 4.0 не строится. Ядро не спрашивает у плагина settings_schema(), и оболочке её не передаёт. Схема сейчас работает внутри плагина: из неё self.setting() берёт умолчания. Если пользователю нужно что-то менять, дайте поле или кнопки на своей вкладке, как notes.
Поля схемы
| Функция | Вид | Что это |
|---|---|---|
Toggle(key, label, default=False, description='', icon='🔘') | toggle | Переключатель: да или нет. |
Text(key, label, default='', description='', icon='✏️') | text | Строка. |
Choice(key, label, options, default=None, description='', icon='📋') | choice | Выбор из списка options. Умолчание — первый вариант. |
Slider(key, label, min=0, max=100, default=None, step=1, description='', icon='🎚️') | slider | Число от min до max с шагом step. Умолчание — min. |
Свои модули и зависимости
Плагин запускается из корня программы, и пакет plugins виден ему целиком. Свой модуль рядом с main.py импортируйте через него: from plugins.convert.units import convert, как в convert. Для этого имя папки должно быть допустимым именем Python.
Доступны стандартная библиотека Python 3.12 и то, что установлено для ядра; core.tools.Param и plugins.* плагины используют открыто. Поля для зависимостей в манифесте нет, и pip во встроенном Python нет. Нужна небольшая библиотека на чистом Python — положите её в папку плагина и импортируйте так же, через plugins.<папка>.
Внутренности ядра за пределами core.tools — не API: они меняются без предупреждения, а плагин, который на них опирается, сломается на следующем выпуске.
Установка и обновление
Ставить можно папку или архив .zip; в архиве плагин может лежать в корне или в одной вложенной папке. Перед копированием ядро проверяет:
- есть
plugin.jsonиmain.py, манифест читается как JSON; - имя подходит под правило из раздела «Манифест», и папка ложится прямо внутрь
plugins/; - архив распаковывается не больше чем в 50 МиБ: размер считается до распаковки.
Установка поверх плагина с тем же именем заменяет его папку целиком и выключает его. Новый код запускается только после того, как пользователь сам включит плагин снова: подложенный архив с чужим именем не наследует чужое согласие.
Обе установки пишутся в журнал безопасности.
Совместимость
Версия API растёт на несовместимых изменениях. Ядро загружает плагины с api_version от 4 до 4. Нижняя граница совпадает с текущей нарочно: плагин версий 1–3 отдавал готовый виджет Qt, а оболочку на C# такой виджет не нарисует.
Несовпадение не молчит. Пользователь увидит в списке плагинов одну из этих строк:
| Когда | Что написано |
|---|---|
| Плагин старше | Плагин написан под API 3, а нужна версия 4: страница описывается методом page(), а не create_page(). Обновите плагин. |
| Плагин новее | Плагину нужна версия API 5, а эта сборка поддерживает 4. Обновите Рину. |
Схема вкладки версионируется отдельно (сейчас 2). Новый вид элемента несовместимостью не считается: незнакомый вид оболочка обязана показать вместе с его именем.
Провод
Писать это руками не нужно, всё делает plugins/host.py. Раздел для тех, кто отлаживает плагин или хочет понимать, что происходит.
Кадр — четыре байта длины (big-endian) и JSON в UTF-8, тот же конверт, что у оболочки с ядром (протокол, §2–3). Кадр больше 1 МиБ ядро не читает и останавливает плагин.
Ядро → плагин
| Метод | Что делает |
|---|---|
plugin.hello | Представиться: манифест, версия API, есть ли вкладка и плитка, описание инструментов. |
plugin.enable | Вызвать on_enable. |
plugin.disable | Вызвать on_disable. |
plugin.command | Вызвать on_command(text); ответ — взята ли фраза. |
plugin.event | Вызвать on_event(name, data). |
plugin.page | Собрать вкладку: page(). |
plugin.home | Собрать плитку: home(). |
plugin.action | Вызвать on_action и вернуть вкладку заново. |
plugin.call | Вызвать run инструмента по имени. |
plugin.shutdown | Попрощаться перед завершением процесса. |
Плагин → ядро
Плагин может сказать ядру ровно пять вещей, и ни одна не касается мира за пределами Рины.
| Метод | Откуда |
|---|---|
plugin.respond | ctx.respond |
plugin.log | ctx.log |
plugin.notify | ctx.notify |
plugin.setting.get | ctx.get_setting; единственный запрос с ответом |
plugin.setting.set | ctx.set_setting |
На неизвестный метод любая сторона отвечает ошибкой: молчание ядро приняло бы за зависание.
Граница
Отдельный процесс — это не песочница. Плагин запускается тем же интерпретатором, с правами того же пользователя. subprocess, сеть и файлы внутри плагина работают.
Что разделение даёт на самом деле:
- инструменты плагина проходят те же проверки, что встроенные: разрешения, подтверждение, журнал с именем плагина;
- часть разрешений не выдаётся никогда, и инструмент, который их просит, не заводится;
- обратиться к ядру плагин может только за пятью вещами из раздела «Провод»;
- зависший или упавший плагин не забирает с собой Рину и соседей.
Поэтому ставить чужой плагин — то же решение, что запустить скачанную программу: смотрите, кто его написал. Угрозы этой поверхности разобраны в модели угроз: T-06, T-07, T-08, T-09, T-10.
Проверка и неполадки
В репозитории программы есть проверка плагинов:
python tools/test_plugins.pyОна поднимает настоящий менеджер, ставит плагин, включает его, просит вкладку, нажимает кнопку и убеждается, что сосед цел, когда один плагин завис.
| Что видно | Почему |
|---|---|
| Плагин не ответил на приветствие. | Процесс не поднялся за 20 с: ошибка при импорте main.py (смотрите журнал плагина) или тяжёлая работа на уровне модуля. |
| «Плагин не ответил за 10 с и был остановлен.» | Хук работал дольше срока. Долгое — в поток. |
| Плагин упал. | Процесс завершился сам: исключение вне хука, sys.exit, нехватка памяти. |
| Плагин прислал слишком большое сообщение… | Вкладка или ответ больше 1 МиБ. |
| Плагин написан под API… | Не указан или устарел api_version, см. «Совместимость». |
| Инструмент не заведён | Он просит разрешение, которого нет в манифесте или которое плагину не выдаётся. |
Пример целиком
Список покупок: фраза, два инструмента, вкладка с полем и плитка, которая появляется, только когда есть что купить. Манифест — в разделе «Манифест». Этот код запускается на настоящем API при каждой сборке страницы, так что он работает с той версией, что описана выше.
main.py
"""A shopping list: a phrase, two tools, a tab and a home tile."""
from core.tools import Param
from plugins.api import Plugin, PluginTool
from plugins.page_spec import Badge, Button, Card, Input, Items, Note, Row
#: Phrases the plugin takes. Narrow on purpose: plugins hear a phrase
#: before the built-in commands do.
OPENERS = ("добавь в покупки ", "в покупки ")
class ShoppingPlugin(Plugin):
page_title = "Покупки"
# --- the list lives in the plugin's settings, kept by the core ------
def _items(self):
return list(self.ctx.get_setting("items", []) or [])
def _add(self, what):
what = what.strip()
if not what:
return "Что добавить?"
items = self._items()
if what.lower() in (one.lower() for one in items):
return f"«{what}» уже в списке."
self.ctx.set_setting("items", items + [what])
return f"Добавила «{what}»."
def _clear(self):
count = len(self._items())
self.ctx.set_setting("items", [])
return f"Список очищен, было {count}."
# --- a phrase -------------------------------------------------------
def on_command(self, text):
low = text.lower().strip()
for opener in OPENERS:
if low.startswith(opener):
self.respond(self._add(text.strip()[len(opener):]))
return True
return False
# --- tools the core may call ----------------------------------------
def tools(self):
return [
PluginTool(
name="add",
summary="Добавить строку в список покупок.",
params=(Param("what", "string", "Что купить, например «молоко»."),),
run=lambda args: self._add(str(args.get("what", ""))),
),
PluginTool(
name="clear",
summary="Очистить список покупок.",
confirm_required=True,
run=lambda args: self._clear(),
),
]
# --- the tab --------------------------------------------------------
def page(self):
items = self._items()
return [
Card([
Items(items) if items else Note("Список пуст."),
Input("add", placeholder="Что купить", button="Добавить"),
], title="Список"),
Row([
Badge(f"В списке: {len(items)}", "good" if items else "normal"),
Button("Очистить", action="clear", variant="danger"),
]),
]
def on_action(self, action, value=None):
if action == "add":
self._add(str(value or ""))
elif action == "clear":
self._clear()
# --- the home tile: only when there is something to buy -------------
def home(self):
items = self._items()
if not items:
return []
shown = ", ".join(items[:3]) + ("…" if len(items) > 3 else "")
return [Card([Note(shown)], title=f"Купить: {len(items)}")]Кнопка «Очистить» на вкладке стирает список сразу: пользователь нажал её сам. Инструмент clear просит подтверждение: его вызов может прийти не из рук пользователя.
Что уходит оболочке
Вкладка после «добавь в покупки молоко», поля «хлеб» и инструмента с «сыр», так, как её получает оболочка:
[
{
"kind": "card",
"text": "Список",
"children": [
{
"kind": "items",
"items": [
"молоко",
"хлеб",
"сыр"
]
},
{
"kind": "input",
"text": "Что купить",
"action": "add",
"variant": "Добавить",
"value": ""
}
]
},
{
"kind": "row",
"children": [
{
"kind": "badge",
"text": "В списке: 3",
"variant": "good"
},
{
"kind": "button",
"text": "Очистить",
"action": "clear",
"variant": "danger"
}
]
}
]Инструменты, как их описание уходит ядру при приветствии:
[
{
"name": "add",
"summary": "Добавить строку в список покупок.",
"permissions": [],
"confirm_required": false,
"params": [
{
"name": "what",
"type": "string",
"description": "Что купить, например «молоко».",
"required": true
}
]
},
{
"name": "clear",
"summary": "Очистить список покупок.",
"permissions": [],
"confirm_required": true,
"params": []
}
]
Плагины в поставке
Всё, что лежит в plugins/ программы, — рабочие образцы. Колонка «Хуки» собрана по их коду.
| Плагин | Что делает | Хуки | Разрешения |
|---|---|---|---|
| Часы | Сообщает текущее время и дату по запросу («сколько времени», «какая дата»). | on_command, tools | — |
| Пересчёт | Переводит величины: «5 км в мили», «20 °C в °F», «3 кг в фунты». Поле прямо на главном экране. | on_command, tools, home, on_action | — |
| Кубик | Бросает игральный кубик или монетку по команде («брось кубик», «подбрось монету»). | on_command, tools | — |
| Приветствие | Отвечает на приветствия. Скажите «привет» — и Рина ответит. | on_enable, on_command | — |
| Заметки | Быстрые заметки: своя вкладка, настройки и команда «запиши…». | on_enable, on_command, tools, page, on_action, settings_schema | — |
| Курс | Курс доллара и евро по Центробанку — плиткой на главном экране и по вопросу вслух. | on_enable, on_command, tools, home | network.external |