Справка по API плагинов
Плагин — это папка, содержащая manifest.json и один JavaScript-модуль. Эта страница — контракт разработки; тот же контракт поставляется как файл объявления TypeScript (types/plugin-api.d.ts) в репозитории каталога, поэтому редакторы автодополняют всё нижеописанное.
Анатомия
my-plugin/
manifest.json
main.js # один самодостаточный ES-модульmain.js экспортирует по умолчанию объект жизненного цикла. Всё, что может достичь плагин, приходит через контекст, переданный в activate; каждый register* и on вызов возвращает одноразовый объект, который приложение освобождает, когда плагин отключен или удален, поэтому deactivate нужно только освобождать собственные внешние ресурсы плагина.
export default {
activate(ctx) {
// регистрировать вклады через ctx
},
deactivate() {
// опционально: закрыть сокеты, сбросить очереди
},
};Включение и отключение вступают в силу немедленно — без перезапуска приложения. Пишите на TypeScript, если хотите (рекомендуется; см. Публикация) — что приложение загружает, всегда является собранным main.js.
manifest.json
{
"id": "anki-sync",
"name": "Anki Sync",
"version": "0.1.0",
"minAppVersion": "0.3.0",
"description": "Отправка просмотренных слов в Anki.",
"author": "вы",
"permissions": ["service:network", "annotations:read"],
"main": "main.js"
}| Поле | Значение |
|---|---|
id | Строчные буквы, цифры, дефисы (макс. 64). Должен совпадать с именем папки; пространство имён для хранилища и инструментов плагина. |
name, version | Показываются в Настройки → Плагины и в каталоге. |
minAppVersion | Самая низкая версия приложения, которую поддерживает плагин. Этот контракт требует 0.3.0 или новее. |
permissions | Что использует плагин (таблица ниже). Показывается пользователю перед установкой. |
main | Входной модуль относительно папки; по умолчанию main.js. |
settings | Опциональные декларативные настройки (те же формы полей, что и представления форм, плюс secret). Приложение отображает их как собственный раздел плагина в Настройках и сохраняет значения как один объект под ключом хранилища settings — см. Хранилище и настройки. |
schedules | Опциональные повторяющиеся задачи, объявленные так, чтобы пользователи видели их перед установкой — см. Запланированная работа. |
themes, fonts | Опциональные декларативные темы и встроенные шрифты (требует ui:themes) — см. Темы и встроенные шрифты. |
Доменная модель
Поверхность данных производна от доменной модели приложения, а не описывается где-то рядом с ней. Каждый домен — shelf (всё управление библиотекой: книги, коллекции, статистика чтения), annotations, conversations — это пространство имён на ctx, предоставляющее три вещи:
- чтения — модели чтения домена (что отображают собственные поверхности приложения);
- записи — команды под
.write, которые точно отражают глаголы событий домена и проходят через собственный путь записи событий приложения, с отметкойplugin:<id>в журнале событий, поэтому каждая запись плагина атрибутируема; - подписки —
.on(event, handler)на события домена под их каноническими именами (book.starred,highlight.created, …) — тот же словарь, который записывает само приложение.
Разрешения следуют той же форме: <domain>:read / <domain>:write, и внутри домена запись подразумевает чтение. Локальное состояние устройства (предпочтения представления, внешний вид ридера, внутренности синхронизации) и свободный рендеринг намеренно не являются поверхностью плагинов — UI идет через декларативные представления ниже.
Разрешения
Группы возможностей на ctx просто отсутствуют, если их разрешение не объявлено — гейтинг на уровне API против случайного превышения полномочий. Хранилище с пространством имён, вклады в UI, события сеанса и навигация ридера не являются разрешениями; каждый плагин имеет их.
| Разрешение | Предоставляет |
|---|---|
shelf:read | ctx.shelf — книги (включая оглавление книги и текст главы), коллекции и членство, и статистику чтения (stats.forBook / stats.list / stats.overview — у статистики нет лица записи: их события — записанные факты активности читателя, а не пользовательские команды). |
shelf:write | ctx.shelf.books.write — импортировать файлы, редактировать метаданные, отмечать звездой, отмечать как завершенную, удалять; поставщики контента и виртуальные книги. ctx.shelf.collections.write — создавать, переименовывать, удалять, назначать книги. |
annotations:read / annotations:write | ctx.annotations — выделения, заметки и заданные вопросы; создавать, перекрашивать, редактировать и удалять выделения и заметки (вопросы — записанные агентом, только для чтения). |
conversations:read | ctx.conversations — потоки AI по книгам и глобальные потоки (только для чтения). |
ui:themes | Декларативные поля манифеста themes / fonts (ниже) — темы приложения и ридера со встроенными шрифтами. Единственный вклад в UI за разрешением: он имеет визуальный авторитет над всем приложением, поэтому согласие на установку должно его раскрывать. |
ui:appearance | ctx.appearance — список всех тем, доступных обеим поверхностям, чтение текущего оформления и переключение темы приложения или цвета страницы. Намеренно отделено от ui:themes: предлагать тему — пассивно, переключать — нет. |
agent:tools | ctx.agent.registerTool — инструменты для читательского ассистента. |
service:network | ctx.network.fetch — исходящий HTTP через нативный клиент приложения (без ограничений CORS). |
service:llm | ctx.llm.ask — одноразовые вызовы модели на настроенной учетной записи пользователя. Без потока, без памяти, без инструментов; поддерживает структурированный JSON-вывод через schema и потоковую передачу через onText. |
service:clipboard | ctx.clipboard.writeText. |
(reader:modes — режимы управляемого чтения, отображаемые хостом — в настоящее время зарезервированы для встроенных первичных плагинов, пока этот привилегированный контракт не устоится.)
Вклады
Действия с выделением
Записи в меню выделения и аннотаций ридера. Обработчик получает выделенный текст, его диапазон CFI, главу и книгу. Когда доступен, context содержит окружающий фрагмент. Внутри ридера действие либо выполняется молча (вернуть toast), либо открывает диалог (вернуть представление) — это единственные два исхода. Объявите presentation: "dialog", когда обработчик асинхронен: хост открывает свою оболочку загрузки немедленно и заполняет тот же запрос, когда run разрешается. Действие в стиле словаря может объявить role: "lookup"; хост тогда направляет свою существующую команду клавиатуры Look up на это действие плагина вместо поддержания второго встроенного пути lookup.
ctx.ui.registerSelectionAction({
id: "save-quote",
title: "Сохранить цитату",
icon: "quotes",
presentation: "dialog",
run: (input) => {
// input: { text, context?, cfiRange, chapterHref, book, source }
return { toast: "Цитата сохранена." };
},
});Действия в шапке
Кнопка-иконка на верхней панели. На поверхности ридера представление открывается как закрепленное всплывающее окно; на полке оно открывается как всплывающее окно или полная страница, согласно presentation. Ридер никогда не позволяет полностраничные прерывания.
ctx.ui.registerHeaderAction({
id: "reading-report",
title: "Отчет о чтении",
icon: "chart-line-up",
surface: "shelf",
presentation: "page",
view: async () => ({
kind: "markdown",
title: "На этой неделе",
markdown: "Вы читали **4ч 12м** в 3 книгах.",
}),
});Команды
Запись в палитре команд. Все действия плагинов также появляются в палитре автоматически; явные команды для действий без кнопки.
ctx.ui.registerCommand({
id: "sync-now",
title: "Anki Sync: синхронизировать сейчас",
run: async () => ({ toast: "Синхронизировано." }),
});Инструменты агента
Инструменты, которые читательский ассистент может вызывать во время чата (требует agent:tools). parameters — это обычная JSON Schema для объекта аргументов; опустите её для инструмента без аргументов. Инструменты имеют пространство имён plugin_<pluginId>_<name> перед тем, как они достигнут модели, и вызовы видны пользователю как шаги инструмента в чате.
ctx.agent?.registerTool({
name: "search_deck",
label: "Поиск в вашей колоде Anki",
description: "Поиск коллекции Anki пользователя по термину.",
parameters: {
type: "object",
properties: { query: { type: "string" } },
required: ["query"],
},
execute: async ({ query }) => {
const res = await ctx.network.fetch("http://127.0.0.1:8765", {
method: "POST",
body: JSON.stringify({ action: "findNotes", query }),
});
return res.json();
},
});Поставщики голосов
ctx.audio.registerVoiceProvider подключает движок текст-в-речь в чтение вслух ридера. Плагин только превращает текст в закодированные аудиобайты (mp3/wav — всё, что декодирует webview); приложение владеет воспроизведением, темпом предложений, предзагрузкой и подсветкой следования. Регистрация не требует собственного разрешения — что бы ни требовалось провайдеру для синтеза (сеть, ключи), уже закрыто его другими разрешениями.
ctx.audio.registerVoiceProvider({
id: "voices",
label: "Мой TTS",
listVoices: () => [{ id: "default", label: "Мой TTS · теплый" }],
synthesize: async ({ text, voiceId }) => {
const res = await ctx.network.fetch("http://127.0.0.1:8880/v1/audio/speech", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ input: text, response_format: "mp3" }),
});
return res.arrayBuffer();
},
});Зарегистрированный голос принимается автоматически — включение вашего плагина пользователем — это опт-ин, нет отдельного выбора на стороне хоста — и неудавшийся вызов синтеза возвращается к системному голосу для этого предложения, поэтому чтение деградирует вместо того, чтобы молчать. Голоса перечисляются заново всякий раз, когда настройки плагина изменяются.
Запланированная работа
Манифест объявляет повторяющиеся задачи; activate привязывает работу. Приложение выполняет каждое расписание КАК МИНИМУМ каждые everyMinutes (округлено до 15), пока оно открыто, с догоняющим запуском вскоре после запуска, когда просрочено — никогда в точные времена, и никогда, пока приложение закрыто. Перекрывающиеся запуски одного расписания пропускаются; неудавшийся запуск просто ждет следующего такта.
// manifest.json
"schedules": [{ "id": "refresh", "label": "Обновить ленты", "everyMinutes": 60 }]
// main.js
ctx.schedule.on("refresh", async () => {
// получить, согласовать, записать через API доменов
});Темы и встроенные шрифты
С ui:themes, манифест может объявлять темы для двух независимых точек монтирования — хром приложения и страницу книги — плюс файлы шрифтов, которые поставляются внутри папки плагина. Этот вклад — чисто данные: приложение проверяет каждое значение и генерирует весь CSS само, и ничего не применяется, пока пользователь не выберет тему в Настройках → Внешний вид или в контроле цвета страницы ридера. main.js плагина только для темы — это просто export default { activate() {} }.
{
"permissions": ["ui:themes"],
"fonts": [
{
"id": "my-serif",
"family": "My Serif",
"kind": "serif",
"files": [{ "path": "assets/my-serif-400.woff2", "weight": 400 }]
}
],
"themes": [
{
"id": "dusk",
"name": { "default": "Сумерки", "translations": { "zh-Hans": "暮色" } },
"polarity": "dark",
"app": { "paper": "#14171e", "fg": "#e3e6ec" },
"reader": {
"palette": {
"bg": "#161a22", "text": "#ccd2dd",
"selection": "rgba(154, 162, 177, 0.28)",
"rule": "rgba(204, 210, 221, 0.18)",
"faint": "rgba(204, 210, 221, 0.07)",
"muted": "rgba(204, 210, 221, 0.55)"
},
"typography": { "fontFamily": "plugin:my-serif", "fontSize": "large" }
}
}
]
}polarity— читается ли тема как светлая или темная. Управляетcolor-scheme, значениями по умолчанию полярности для токенов приложения, которые тема оставляет неустановленными, и как разрешается автоматический цвет страницы ридера, пока тема активна.app— переопределения на фиксированном словаре токенов приложения (холст, текстовые ярусы, поверхности, заливки, границы — см.PluginAppThemeTokensв типизации). Неустановленные токены сохраняют собственные значения полярности.reader— та же шестицветная палитра, которую используют встроенные цвета страницы (все шесть обязательны), плюс опциональная типографская предустановка, применяемая один раз, когда пользователь выбирает тему; пользователь может настраивать всё потом.fonts—.woff2/.woff/.ttf/.otfлица, обслуживаемые прямо из папки плагина; каждое появляется в выборе шрифта ридера, пока плагин включен. Тема ссылается на свои собственные шрифты какplugin:<fontId>. Плагины каталога должны перечислять файлы шрифтов вfilesзаписи реестра.- Цвета проверяются по строгим грамматикам — простой hex или
rgb()/rgba()/hsl()/hsla(); ключевые слова,var()иurl()отклоняются.
Представления
Плагины объявляют дерево компонентов хоста; приложение отображает каждый визуальный примитив и контрол. Плагины никогда не предоставляют JSX, HTML, CSS или классы.
markdown— строка markdown, набранная приложением.list— списки хоста с поиском и фиксированной задержкой, ключевыми словами, аксессуарами и пустыми состояниями.timelineдобавляет фильтры Сегодня / На этой неделе / В этом месяце / Все и группировку по локальной дате; элемент может использоватьpresentation: "dialog", чтобы показать своё возвращенное представление над списком вместо открытия дочерней страницы.actionsуровня списка — кнопки-иконки, отображаемые хостом; временные шкалы размещают их в крайнем правом углу строки вкладок.form— контролы text, textarea, number, time, select, choice, checkbox и toggle из библиотеки компонентов ReadAware, плюсonSubmit.detail— первичный контент в стиле Raycast, метаданные и контролы и действия, отображаемые хостом. Семантические контролы выбора остаются возле заголовка контента; диалоги сохраняют происхождение, даты и теги в тихой строке под ним, в то время как действия сидят рядом с кнопкой Закрыть хоста в фиксированном футере.blocks— типография хоста, markdown, контент словаря, метаданные, цитаты, действия, метрики, прогресс, теги, оповещения, разделы, группы и адаптивныеcolumns. Колонки предоставляют только ограниченный вес, интервал, предустановки минимальной ширины и семантическое выравнивание. Точный CSS и обтекание остаются внутри дизайн-системы; объявления проверяются во время выполнения, а вложенность ограничена.
Обработчики (run, onSelect, onSubmit) все возвращают ту же форму результата:
- ничего — поверхность остается такой, какая она есть;
{ toast: "…" }— временное уведомление;{ view }— открыть или поместить на поверхность;{ view, navigation: "replace" | "reset" }— заменить текущее представление или вернуться к новому корневому представлению;{ close: true }— закрыть поверхность (компонуется сtoast);{ fieldErrors }— из отправки формы: остаться на форме и показать ошибки под полями.
Асинхронная работа — не событие: верните промис, и приложение покажет состояние загрузки. Иконки выбираются по имени из курированного набора Phosphor приложения — без пользовательских SVG.
Данные домена
Каждое предоставленное пространство имен домена предлагает чтения, канонические подписки на события и (с разрешением на запись) команды. Кратко:
ctx.shelf.books—list(),get(id),getToc(id),getChapterText(id, index); запись:import,editMetadata,setStarred,setFinished,remove, плюс поставщики контента (ниже).ctx.shelf.collections—list(),booksIn(id); запись:create,rename,remove,assignBooks(bookIds, collectionId | null).ctx.shelf.stats—forBook(bookId),list(),overview()(позиции, статусы и активное время чтения; только для чтения для каждого актора).ctx.annotations—list({ bookId?, kind?, query? })возвращает дискриминированное объединение выделений, заметок и вопросов; запись:createHighlight,recolorHighlight,removeHighlight,createNote,updateNote,removeNote.ctx.conversations—getBookThread(bookId),listThreads(),getThread(id); подписаться черезon(aiConversation.started,aiMessage.appended,aiMessage.removed,aiConversation.cleared).
События
Два класса, намеренно отдельных. События домена — факты, которые записывает приложение; подписывайтесь по домену, под каноническими именами, с разрешением на чтение домена. Каждая доставка — { type, payload, createdAt, origin } — origin говорит, какой программный актор произвел факт (user, agent, system или plugin:<id>).
ctx.annotations?.on("highlight.created", ({ payload, origin }) => {
// payload: { highlightId, bookId, text, color?, … }
});
ctx.shelf?.on("book.removed", ({ payload }) => { /* { bookId } */ });
Факты сеанса описывают, что на экране прямо сейчас. Они никогда не входят в журнал событий и не требуют разрешения: ctx.session.on(event, handler).
| Событие сеанса | Payload |
|---|---|
book-opened | { book: { id, title, author? } } |
book-closed | { bookId } |
chapter-changed | { bookId, chapterHref } |
reading-progress | { bookId, fraction } — срабатывает при перелистывании страниц, дробь 0..1 |
Поставщики контента и виртуальные книги
С shelf:write, плагин может размещать реальные книги на полке. import принимает байты файла. Поставщики контента полностью пропускают файл: зарегистрируйте провайдера, добавьте виртуальные книги, связанные с ним, и обслуживайте HTML-секции, когда книга открыта. Ридер пагинирует, аннотирует и отслеживает прогресс на них, как на любой книге — RSS-лента как книга — это именно это.
ctx.shelf?.books.write?.registerContentProvider({
id: "rss",
async load(key) {
const feed = await fetchFeed(key); // ваш код, через ctx.network.fetch
return {
title: feed.title,
sections: feed.items.map((item) => ({
title: item.title,
html: item.contentHtml,
})),
};
},
});
await ctx.shelf?.books.write?.addVirtualBook({
providerId: "rss",
key: "https://example.com/feed.xml",
title: "Example Weekly",
});Хранилище и настройки
ctx.storage — это хранилище ключ-значение с пространством имён, сохраняемое с локальными данными приложения — get, set, remove. Если манифест объявляет поля settings, приложение отображает их как собственный раздел плагина в Настройках, а значения приходят в ctx.storage.get("settings") как один объект. Читательский ассистент также может просматривать и изменять эти настройки (поля, отмеченные agentHidden, остаются вне его поля зрения). Три возможности полей выходят за рамки простой формы:
visibleWhen: { field, equals }показывает поле только пока другое поле содержит одно из указанных значений. Скрытые поля сохраняют свои сохраненные значения — один объект настроек может нести значение, установленное для каждого варианта (плагин TTS сохраняет один голос для каждого провайдера таким образом).selectсdynamicOptions: trueразрешает свои опции во время выполнения: привяжите источник вactivateсctx.settings.provideOptions(fieldId, async (values) => [...]). Когда источник ничего не выдает (пока нет учетных данных, конечная точка недостижима), поле возвращается к вводу свободного текста — перечисление — это удобство, никогда не ворота.kind: "secret"объявляет учетные данные: приложение отображает ввод пароля, записывающий в зашифрованное секретное хранилище — id поля ЯВЛЯЕТСЯ ключомctx.secrets, который ваш код читает обратно — никогда в простые настройки и никогда в каталог ассистента. Сохраненное значение никогда не отображается; поле показывает настроенное состояние и доступность очистки.
Для структурированных данных ctx.storage.collection(name) открывает именованную коллекцию документов — put / get / delete / list над записями документов, с опциональным происхождением bookId / anchor, по которому вы можете фильтровать. Происхождение — это индекс, а не владение: документы выживают удалению ссылающейся книги, и жизненный цикл коллекции принадлежит плагину (удаление очищает её). Встроенный плагин Словарь и его хронология сохраненных слов полностью построены на этом уровне.
Внешний контекст
Всегда доступен, разрешение не требуется:
ctx.manifest,ctx.appVersion,ctx.locale(текущая локаль BCP-47 UI приложения — читайте её во время использования, она отслеживает языковую настройку в реальном времени);ctx.ui.showToast(message);ctx.ui.exportFile({ filename, content, mimeType? })открывает поток сохранения хоста для генерированного текста (CSV, JSON, Markdown) или бинарных байт;ctx.secrets— зашифрованное хранилище учетных данных, с пространством имён для каждого плагина (токены API и подобное); живет вне SQLite и резервных копий и выживает удалению;ctx.session.on(…)— факты сеанса выше;ctx.reader.openBook(bookId)иctx.reader.goTo({ bookId?, cfi?, href? })— навигация ридером (пользовательский контроль, без раскрытия данных).
Стабильность
Это контракт v2, поставленный в приложении 0.3.0 — намеренная ломающая перестройка, которая вывела всю поверхность из доменной модели (манифесты v1 не проходят установку с читаемой ошибкой). Отсюда API растет аддитивно: новые домены, новые имена событий, новые виды блоков — декларативные темы (ui:themes) — первое такое дополнение. Ломающие изменения в том, что здесь задокументировано, рассматриваются как ошибки. Объявляйте minAppVersion для всего, что зависит от недавнего дополнения.