ReadAware

Справка по 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:readctx.shelf — книги (включая оглавление книги и текст главы), коллекции и членство, и статистику чтения (stats.forBook / stats.list / stats.overview — у статистики нет лица записи: их события — записанные факты активности читателя, а не пользовательские команды).
shelf:writectx.shelf.books.write — импортировать файлы, редактировать метаданные, отмечать звездой, отмечать как завершенную, удалять; поставщики контента и виртуальные книги. ctx.shelf.collections.write — создавать, переименовывать, удалять, назначать книги.
annotations:read / annotations:writectx.annotations — выделения, заметки и заданные вопросы; создавать, перекрашивать, редактировать и удалять выделения и заметки (вопросы — записанные агентом, только для чтения).
conversations:readctx.conversations — потоки AI по книгам и глобальные потоки (только для чтения).
ui:themesДекларативные поля манифеста themes / fonts (ниже) — темы приложения и ридера со встроенными шрифтами. Единственный вклад в UI за разрешением: он имеет визуальный авторитет над всем приложением, поэтому согласие на установку должно его раскрывать.
ui:appearancectx.appearance — список всех тем, доступных обеим поверхностям, чтение текущего оформления и переключение темы приложения или цвета страницы. Намеренно отделено от ui:themes: предлагать тему — пассивно, переключать — нет.
agent:toolsctx.agent.registerTool — инструменты для читательского ассистента.
service:networkctx.network.fetch — исходящий HTTP через нативный клиент приложения (без ограничений CORS).
service:llmctx.llm.ask — одноразовые вызовы модели на настроенной учетной записи пользователя. Без потока, без памяти, без инструментов; поддерживает структурированный JSON-вывод через schema и потоковую передачу через onText.
service:clipboardctx.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.bookslist(), get(id), getToc(id), getChapterText(id, index); запись: import, editMetadata, setStarred, setFinished, remove, плюс поставщики контента (ниже).
  • ctx.shelf.collectionslist(), booksIn(id); запись: create, rename, remove, assignBooks(bookIds, collectionId | null).
  • ctx.shelf.statsforBook(bookId), list(), overview() (позиции, статусы и активное время чтения; только для чтения для каждого актора).
  • ctx.annotations list({ bookId?, kind?, query? }) возвращает дискриминированное объединение выделений, заметок и вопросов; запись: createHighlight, recolorHighlight, removeHighlight, createNote, updateNote, removeNote.
  • ctx.conversationsgetBookThread(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 для всего, что зависит от недавнего дополнения.