Referencia API de plugins
Un plugin es una carpeta que contiene un manifest.json y un módulo JavaScript. Esta página es el contrato de creación; el mismo contrato se envía como un archivo de declaración TypeScript (types/plugin-api.d.ts) en el repositorio del Mercado, por lo que los editores autocompletarán todo lo que sigue.
Anatomía
my-plugin/
manifest.json
main.js # un módulo ES autocontenidomain.js exporta por defecto un objeto de ciclo de vida. Todo lo que un plugin puede alcanzar viene a través del contexto entregado a activate; cada llamada a register* y on devuelve un desechable que la aplicación reclama cuando el plugin se deshabilita o desinstala, por lo que deactivate solo necesita liberar los recursos externos propios del plugin.
export default {
activate(ctx) {
// registrar contribuciones vía ctx
},
deactivate() {
// opcional: cerrar sockets, vaciar colas
},
};Habilitar y deshabilitar surte efecto inmediatamente — sin reiniciar la aplicación. Escribe en TypeScript si quieres (recomendado; ver Publicación) — lo que la aplicación carga es siempre el main.js compilado.
manifest.json
{
"id": "anki-sync",
"name": "Anki Sync",
"version": "0.1.0",
"minAppVersion": "0.3.0",
"description": "Send looked-up words to Anki.",
"author": "you",
"permissions": ["service:network", "annotations:read"],
"main": "main.js"
}| Campo | Significado |
|---|---|
id | Letras minúsculas, dígitos, guiones (máx. 64). Debe igualar el nombre de la carpeta; actúa como espacio de nombres para el almacenamiento y herramientas del plugin. |
name, version | Mostrados en Ajustes → Plugins y el Mercado. |
minAppVersion | Versión mínima de aplicación que el plugin soporta. Este contrato requiere 0.3.0 o más reciente. |
permissions | Lo que usa el plugin (tabla abajo). Se muestra al usuario antes de la instalación. |
main | Módulo de entrada relativo a la carpeta; por defecto main.js. |
settings | Configuración declarativa opcional (mismas formas de campo que las vistas de formulario, más secret). La aplicación las renderiza como la propia sección del plugin en Ajustes y persiste los valores como un objeto bajo la clave de almacenamiento settings — ver Almacenamiento y configuración. |
schedules | Tareas recurrentes opcionales, declaradas para que los usuarios las vean antes de instalar — ver Trabajo programado. |
themes, fonts | Temas declarativos opcionales y fuentes incluidas (requiere ui:themes) — ver Temas y fuentes incluidas. |
El modelo de dominio
La superficie de datos se deriva del modelo de dominio de la aplicación en lugar de ser escrita junto a él. Cada dominio — shelf (la totalidad de la gestión de biblioteca: libros, colecciones, estadísticas de lectura), annotations, conversations — es un espacio de nombres en ctx que expone tres cosas:
- lecturas — los modelos de lectura del dominio (lo que las propias superficies de la aplicación renderizan);
- escrituras — comandos bajo
.writeque reflejan exactamente los verbos de eventos del dominio y pasan por la propia ruta de escritura basada en eventos de la aplicación, estampadosplugin:<id>en el registro de eventos para que cada escritura de plugin sea atribuible; - suscripciones —
.on(event, handler)sobre los eventos del dominio bajo sus nombres canónicos (book.starred,highlight.created, …) — el mismo vocabulario que la aplicación registra.
Los permisos siguen la misma forma: <domain>:read / <domain>:write, y dentro de un dominio write implica read. El estado local del dispositivo (preferencias de vista, apariencia del lector, internos de sincronización) y la renderización de forma libre están deliberadamente fuera de la superficie de plugins — la UI pasa por las vistas declarativas a continuación.
Permisos
Los grupos de capacidad en ctx simplemente están ausentes a menos que su permiso esté declarado — control a nivel de API contra excesos accidentales. El almacenamiento con espacio de nombres, las contribuciones de UI, los eventos de sesión y la navegación del lector no son permisos; cada plugin los tiene.
| Permiso | Otorga |
|---|---|
shelf:read | ctx.shelf — libros (incl. tabla de contenidos de un libro y texto de capítulo), colecciones y membresía, y estadísticas de lectura (stats.forBook / stats.list / stats.overview — las estadísticas no tienen cara de escritura: sus eventos son hechos registrados de actividad del lector, no comandos de usuario). |
shelf:write | ctx.shelf.books.write — importar archivos, editar metadatos, marcar con estrella, marcar como terminado, eliminar; proveedores de contenido y libros virtuales. ctx.shelf.collections.write — crear, renombrar, eliminar, asignar libros. |
annotations:read / annotations:write | ctx.annotations — subrayados, notas y preguntas hechas; crear, recolorear, editar y eliminar subrayados y notas (las preguntas son escritas por el agente, solo lectura). |
conversations:read | ctx.conversations — hilos de IA por libro e hilos globales (solo lectura). |
ui:themes | Los campos declarativos del manifest themes / fonts (abajo) — temas de aplicación y lector con fuentes incluidas. La única contribución de UI detrás de un permiso: tiene autoridad visual sobre toda la aplicación, por lo que el consentimiento de instalación debe mencionarlo. |
ui:appearance | ctx.appearance — listar todos los temas que ofrecen ambas superficies, leer la apariencia actual y cambiar el tema de la aplicación o el color de página. Deliberadamente separado de ui:themes: ofrecer un tema es pasivo, cambiarlo no. |
agent:tools | ctx.agent.registerTool — herramientas para el asistente de lectura. |
service:network | ctx.network.fetch — HTTP saliente a través del cliente nativo de la aplicación (sin restricciones CORS). |
service:llm | ctx.llm.ask — llamadas únicas al modelo en la cuenta configurada del usuario. Sin hilo, sin memoria, sin herramientas; soporta salida JSON estructurada vía schema y streaming vía onText. |
service:clipboard | ctx.clipboard.writeText. |
(reader:modes — modos de lectura guiada renderizados por el host — está actualmente reservado para los plugins de primera parte incluidos mientras ese contrato privilegiado se asienta.)
Contribuciones
Acciones de selección
Entradas en los menús de selección y anotación del lector. El manejador recibe el texto seleccionado, su rango CFI, el capítulo y el libro. Cuando está disponible, context contiene el pasaje circundante. Dentro del lector una acción se ejecuta silenciosamente (devuelve un toast) o abre un diálogo (devuelve una vista) — esos son los únicos dos resultados. Declara presentation: "dialog" cuando el manejador es async: el host abre su shell de carga inmediatamente y llena la misma solicitud cuando run se resuelve. Una acción estilo diccionario puede declarar role: "lookup"; el host entonces enruta su comando de teclado Buscar existente a esa acción de plugin en lugar de mantener una segunda ruta de búsqueda incorporada.
ctx.ui.registerSelectionAction({
id: "save-quote",
title: "Save quote",
icon: "quotes",
presentation: "dialog",
run: (input) => {
// input: { text, context?, cfiRange, chapterHref, book, source }
return { toast: "Quote saved." };
},
});Acciones de encabezado
Un botón de icono en una barra superior. En la superficie del lector la vista se abre como un popover anclado; en la estantería se abre como un popover o una página completa, según presentation. El lector nunca permite interrupciones de página completa.
ctx.ui.registerHeaderAction({
id: "reading-report",
title: "Reading report",
icon: "chart-line-up",
surface: "shelf",
presentation: "page",
view: async () => ({
kind: "markdown",
title: "This week",
markdown: "You read **4h 12m** across 3 books.",
}),
});Comandos
Una entrada de paleta de comandos. Todas las acciones de plugins también aparecen en la paleta automáticamente; los comandos explícitos son para acciones sin botón.
ctx.ui.registerCommand({
id: "sync-now",
title: "Anki Sync: sync now",
run: async () => ({ toast: "Synced." }),
});Herramientas de agente
Herramientas que el asistente de lectura puede llamar durante el chat (requiere agent:tools). parameters es un JSON Schema simple para el objeto de argumentos; omítelo para una herramienta sin argumentos. Las herramientas tienen espacio de nombres plugin_<pluginId>_<name> antes de llegar al modelo, y las llamadas son visibles para el usuario como pasos de herramienta en el chat.
ctx.agent?.registerTool({
name: "search_deck",
label: "Searching your Anki deck",
description: "Search the user's Anki collection for a term.",
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();
},
});Proveedores de voz
ctx.audio.registerVoiceProvider conecta un motor de texto a voz en la lectura en voz alta del lector. El plugin solo convierte texto en bytes de audio codificado (mp3/wav — cualquier cosa que decodifique el webview); la aplicación posee la reproducción, el ritmo de oraciones, la precarga y el resaltado de seguimiento. El registro no necesita permiso propio — lo que el proveedor necesite para sintetizar (red, claves) ya está controlado por sus otros permisos.
ctx.audio.registerVoiceProvider({
id: "voices",
label: "My TTS",
listVoices: () => [{ id: "default", label: "My TTS · warm" }],
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();
},
});Una voz registrada se adopta automáticamente — el usuario habilitando tu plugin es la adhesión, no hay selector separado del lado del host — y una llamada de síntesis fallida vuelve a la voz del sistema para esa oración, por lo que la lectura se degrada en lugar de silenciarse. Las voces se relistan cada vez que cambia la configuración del plugin.
Trabajo programado
El manifest declara tareas recurrentes; activate vincula el trabajo. La aplicación ejecuta cada horario AL MENOS cada everyMinutes (piso de 15) mientras está abierta, con una ejecución de recuperación poco después del inicio cuando está atrasada — nunca en tiempos exactos, y nunca mientras la aplicación está cerrada. Las ejecuciones superpuestas de un horario se omiten; una ejecución fallida solo espera la siguiente cadencia.
// manifest.json
"schedules": [{ "id": "refresh", "label": "Refresh feeds", "everyMinutes": 60 }]
// main.js
ctx.schedule.on("refresh", async () => {
// obtener, reconciliar, escribir a través de las APIs de dominio
});Temas y fuentes incluidas
Con ui:themes, el manifest puede declarar temas para dos puntos de montaje independientes — el chrome de la aplicación y la página del libro — más archivos de fuente que se envían dentro de la carpeta del plugin. Esta contribución son datos puros: la aplicación valida cada valor y genera todo el CSS ella misma, y nada se aplica hasta que el usuario elige el tema en Ajustes → Apariencia o el control de color de página del lector. El main.js de un plugin solo de tema es simplemente 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": "Dusk", "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— si el tema se lee como claro u oscuro. Manejacolor-scheme, los valores predeterminados de polaridad para los tokens de aplicación que el tema deja sin configurar, y cómo se resuelve el color de página Auto del lector mientras el tema está activo.app— anulaciones en el vocabulario fijo de tokens de la aplicación (lienzo, niveles de texto, superficies, rellenos, bordes — verPluginAppThemeTokensen las declaraciones). Los tokens no configurados mantienen los valores propios de la polaridad.reader— la misma paleta de seis colores que usan los colores de página integrados (los seis requeridos), más un preset tipográfico opcional aplicado una vez cuando el usuario selecciona el tema; el usuario puede ajustar todo después.fonts— caras.woff2/.woff/.ttf/.otfservidas directamente desde la carpeta del plugin; cada una aparece en el selector de fuentes del lector mientras el plugin está habilitado. Un tema referencia sus propias fuentes comoplugin:<fontId>. Los plugins del Mercado deben listar archivos de fuente en elfilesde la entrada del registro.- Los colores se validan contra gramáticas estrictas — hex simple o
rgb()/rgba()/hsl()/hsla(); las palabras clave,var()yurl()se rechazan.
Vistas
Los plugins declaran un árbol de componentes del host; la aplicación renderiza cada primitiva visual y control. Los plugins nunca proporcionan JSX, HTML, CSS o clases.
markdown— una cadena markdown, compuesta por la aplicación.list— listas de host con búsqueda con debounce fijo, palabras clave, accesorios y estados vacíos.timelineagrega filtros Hoy / Esta semana / Este mes / Todo y grupos de fecha local; un elemento puede usarpresentation: "dialog"para mostrar su vista devuelta sobre la lista en lugar de empujar una página hija. Lasactionsa nivel de lista son botones de icono renderizados por el host; las líneas de tiempo las colocan a la derecha de la fila de pestañas.form— controles text, textarea, number, time, select, choice, checkbox y toggle de la biblioteca de componentes de ReadAware, másonSubmit.detail— contenido primario, metadatos y controles y acciones renderizados por el host al estilo Raycast. Los controles de selección semántica permanecen junto al encabezado de contenido; los diálogos mantienen procedencia, fechas y etiquetas en una línea discreta debajo, mientras que las acciones se sientan junto al botón Cerrar del host en un pie de página fijo.blocks— tipografía del host, markdown, contenido de diccionario, metadatos, citas, acciones, métricas, progreso, etiquetas, alertas, secciones, grupos ycolumnsresponsivas. Las columnas solo exponen peso acotado, espaciado, presets de ancho mínimo y alineación semántica. El CSS exacto y el ajuste de línea permanecen dentro del sistema de diseño; las declaraciones se validan en tiempo de ejecución y el anidamiento está limitado.
Los manejadores (run, onSelect, onSubmit) todos devuelven la misma forma de resultado:
- nada — la superficie permanece como está;
{ toast: "…" }— un aviso transitorio;{ view }— abrir, o empujar sobre, la superficie;{ view, navigation: "replace" | "reset" }— reemplazar la vista actual, o volver a una nueva vista raíz;{ close: true }— descartar la superficie (componible contoast);{ fieldErrors }— desde un envío de formulario: permanecer en el formulario y mostrar errores bajo los campos.
El trabajo async no es un evento: devuelve una promesa y la aplicación muestra el estado de carga. Los iconos se eligen por nombre del conjunto Phosphor curado de la aplicación — sin SVG personalizado.
Datos de dominio
Cada espacio de nombres de dominio otorgado ofrece lecturas, suscripciones de eventos canónicos y (con el permiso de escritura) comandos. En resumen:
ctx.shelf.books—list(),get(id),getToc(id),getChapterText(id, index); escritura:import,editMetadata,setStarred,setFinished,remove, más proveedores de contenido (abajo).ctx.shelf.collections—list(),booksIn(id); escritura:create,rename,remove,assignBooks(bookIds, collectionId | null).ctx.shelf.stats—forBook(bookId),list(),overview()(posiciones, estados y tiempo de lectura activo; solo lectura para cada actor).ctx.annotations—list({ bookId?, kind?, query? })devuelve una unión discriminada de subrayados, notas y preguntas; escritura:createHighlight,recolorHighlight,removeHighlight,createNote,updateNote,removeNote.ctx.conversations—getBookThread(bookId),listThreads(),getThread(id); suscribir víaon(aiConversation.started,aiMessage.appended,aiMessage.removed,aiConversation.cleared).
Eventos
Dos clases, deliberadamente separadas. Eventos de dominio son los hechos que la aplicación registra; suscríbete por dominio, bajo nombres canónicos, con el permiso de lectura del dominio. Cada entrega es { type, payload, createdAt, origin } — origin dice qué actor de software produjo el hecho (user, agent, system, o plugin:<id>).
ctx.annotations?.on("highlight.created", ({ payload, origin }) => {
// payload: { highlightId, bookId, text, color?, … }
});
ctx.shelf?.on("book.removed", ({ payload }) => { /* { bookId } */ });
Hechos de sesión describen lo que está en pantalla ahora mismo. Nunca entran al registro de eventos y no necesitan permiso: ctx.session.on(event, handler).
| Evento de sesión | Carga útil |
|---|---|
book-opened | { book: { id, title, author? } } |
book-closed | { bookId } |
chapter-changed | { bookId, chapterHref } |
reading-progress | { bookId, fraction } — se dispara al pasar páginas, fracción 0..1 |
Proveedores de contenido y libros virtuales
Con shelf:write, un plugin puede poner libros reales en la estantería. import toma los bytes de un archivo. Los proveedores de contenido omiten el archivo por completo: registra un proveedor, agrega libros virtuales vinculados a él, y sirve secciones HTML cuando se abre el libro. El lector pagina, anota y rastrea el progreso en ellos como cualquier libro — un feed RSS como libro es exactamente esto.
ctx.shelf?.books.write?.registerContentProvider({
id: "rss",
async load(key) {
const feed = await fetchFeed(key); // tu código, vía 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",
});Almacenamiento y configuración
ctx.storage es un almacén clave-valor con espacio de nombres persistido con los datos locales de la aplicación — get, set, remove. Si el manifest declara campos settings, la aplicación los renderiza como la propia sección del plugin en Ajustes y los valores llegan a ctx.storage.get("settings") como un objeto. El asistente de lectura también puede ver y cambiar estas configuraciones (los campos marcados agentHidden permanecen fuera de su vista). Tres capacidades de campo van más allá de un formulario simple:
visibleWhen: { field, equals }muestra un campo solo mientras otro campo contiene uno de los valores dados. Los campos ocultos mantienen sus valores almacenados — un objeto de configuración puede llevar un valor configurado por variante (el plugin TTS mantiene una voz por proveedor de esta manera).- Un
selectcondynamicOptions: trueresuelve sus opciones en tiempo de ejecución: vincula la fuente enactivateconctx.settings.provideOptions(fieldId, async (values) => [...]). Cuando la fuente no produce nada (sin credenciales aún, endpoint inaccesible) el campo vuelve a entrada de texto libre — listar es una conveniencia, nunca una puerta. kind: "secret"declara una credencial: la aplicación renderiza una entrada de contraseña escribiendo al almacén secreto cifrado — el id del campo ES la clave dectx.secretsque tu código lee de vuelta — nunca a configuración simple, y nunca en el catálogo del asistente. El valor almacenado nunca se hace eco; el campo muestra un estado configurado y un affordance claro.
Para datos estructurados, ctx.storage.collection(name) abre una colección de documentos con nombre — put / get / delete / list sobre registros por documento, con procedencia bookId / anchor opcional por la que puedes filtrar. La procedencia es un índice, no propiedad: los documentos sobreviven la eliminación del libro referenciado, y el ciclo de vida de la colección pertenece al plugin (desinstalar la limpia). El plugin Dictionary incorporado y su línea de tiempo de palabras guardadas están construidos completamente en este nivel.
Contexto ambiente
Siempre disponible, sin permiso necesario:
ctx.manifest,ctx.appVersion,ctx.locale(la locale BCP-47 actual de la UI de la aplicación — léela al momento de uso, rastrea la configuración de idioma en vivo);ctx.ui.showToast(message);ctx.ui.exportFile({ filename, content, mimeType? })abre el flujo de guardado del host para texto generado (CSV, JSON, Markdown) o bytes binarios;ctx.secrets— almacenamiento de credenciales cifrado, con espacio de nombres por plugin (tokens API y similares); vive fuera de SQLite y copias de seguridad y sobrevive la desinstalación;ctx.session.on(…)— los hechos de sesión arriba;ctx.reader.openBook(bookId)yctx.reader.goTo({ bookId?, cfi?, href? })— navegar el lector (control visible al usuario, sin exposición de datos).
Estabilidad
Este es el contrato v2, enviado en la aplicación 0.3.0 — una reconstrucción rompedora deliberada que derivó toda la superficie del modelo de dominio (los manifests v1 fallan la instalación con un error legible). Desde aquí la API crece aditivamente: nuevos dominios, nuevos nombres de eventos, nuevos tipos de bloques — los temas declarativos (ui:themes) son la primera de esas adiciones. Los cambios rompedores a lo que está documentado aquí se tratan como bugs. Declara minAppVersion para cualquier cosa que dependa de una adición reciente.