ReadAware

Référence de l'API Extension

Une extension est un dossier contenant un manifest.json et un module JavaScript. Cette page est le contrat d'écriture ; ce même contrat est publié sous forme de fichier de déclarations TypeScript (types/plugin-api.d.ts) avec le dépôt du marché d'extensions, pour la complétion automatique de l'éditeur sur tout ce qui suit.

Structure

my-plugin/
  manifest.json
  main.js        # Un seul module ES autonome

main.js exporte par défaut un objet de cycle de vie. Tout ce qu'une extension peut toucher provient du contexte passé à activate ; chaque appel register* et on renvoie un objet jetable, que l'application nettoie uniformément quand l'extension est désactivée ou désinstallée, donc deactivate ne doit libérer que les ressources externes propres à l'extension.

export default {
  activate(ctx) {
    // Enregistrer des points de contribution via ctx
  },
  deactivate() {
    // Optionnel : fermer les sockets, vider les files d'attente
  },
};

L'activation et la désactivation prennent effet immédiatement — pas besoin de redémarrer l'application. Écrivez en TypeScript si vous le souhaitez (recommandé ; voir Publication et distribution) — l'application charge toujours le main.js construit.

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"
}
ChampSignification
idMinuscules, chiffres et traits d'union (maximum 64). Doit correspondre au nom du dossier ; sert d'espace de noms pour le stockage et les outils de l'extension.
name, versionAffiché dans « Réglages → Extensions » et le marché d'extensions.
minAppVersionVersion minimale de l'application supportée par l'extension. Ce contrat nécessite 0.3.0 ou plus récent.
permissionsLes capacités utilisées par l'extension (voir tableau ci-dessous). Affiché à l'utilisateur avant l'installation.
mainModule d'entrée relatif au dossier de l'extension ; par défaut main.js.
settingsParamètres déclaratifs optionnels (même forme de champ que les vues de formulaire, plus secret). L'application les rend comme section propre à l'extension, et persiste toutes les valeurs en tant qu'objet sous la clé de stockage settings — voir Stockage et paramètres.
schedulesTâches périodiques optionnelles, déclarées ici pour que les utilisateurs puissent les voir avant l'installation — voir Tâches planifiées.
themes, fontsThèmes déclaratifs optionnels et polices groupées (nécessite la permission ui:themes) — voir Thèmes et polices groupées.

Modèle de domaine

La surface de données est dérivée du modèle de domaine de l'application, pas écrite à côté. Chaque domaine — shelf (toute la gestion de la bibliothèque : catalogue de livres, regroupements et statistiques de lecture), annotations, conversations — est un espace de noms sur ctx, exposant trois choses :

  • Lectures — les modèles de lecture de ce domaine (sur lesquels l'interface de l'application elle-même se rend) ;
  • Écritures — des commandes sous .write, en correspondance stricte un-à-un avec les verbes d'événement de ce domaine, et passant par le propre chemin d'écriture event-sourced de l'application, marqué dans le journal d'événements comme plugin:<id>, donc chaque écriture d'extension est traçable ;
  • Abonnements.on(event, handler), abonnez-vous aux événements de ce domaine par des noms canoniques (book.starred, highlight.created…) — le même vocabulaire avec lequel l'application elle-même enregistre les faits.

Les permissions suivent la même forme : <domain>:read / <domain>:write, et dans un domaine, l'écriture implique la lecture. L'état local de l'appareil (préférences de vue, apparence du lecteur, données internes de synchronisation) et le rendu libre sont délibérément hors de la surface d'extension — toute l'UI passe par les vues déclaratives ci-dessous.

Permissions

Sans la permission correspondante déclarée, le groupe de capacités sur ctx n'existe tout simplement pas — protection au niveau de l'API contre le dépassement involontaire. Le stockage par espace de noms, les points de contribution d'interface, les événements de session et la navigation du lecteur ne sont pas des permissions ; chaque extension les possède.

PermissionAccorde
shelf:readctx.shelf— catalogue de livres (y compris le sommaire d'un livre et le texte des chapitres), regroupements et affectations, et statistiques de lecture (stats.forBook / stats.list / stats.overview— les statistiques n'ont pas de face d'écriture : leurs événements sont les faits de l'activité du lecteur enregistrée, pas de commandes utilisateur).
shelf:writectx.shelf.books.write— importer des fichiers, modifier les métadonnées, marquer en vedette, marquer comme terminé, supprimer ; et les fournisseurs de contenu et livres virtuels.ctx.shelf.collections.write— créer, renommer, supprimer, attribuer des livres aux collections.
annotations:read / annotations:writectx.annotations— surlignages, notes et questions ; créer, recolorer, modifier, supprimer les surlignages et notes (les questions sont écrites par l'assistant, lecture seule).
conversations:readctx.conversations— les fils IA par livre et les fils globaux (lecture seule).
ui:themesChamps déclaratifs themes / fonts dans le manifest (voir ci-dessous) — thèmes d'application et de lecture, avec des polices facultatives. C'est le seul point de contribution d'UI nécessitant une permission : il a un impact visuel sur toute l'application, la confirmation d'installation doit le mettre en lumière.
ui:appearancectx.appearance — lister tous les thèmes proposés par les deux surfaces, lire l’apparence actuelle et changer le thème de l’application ou la couleur de page. Volontairement distinct de ui:themes : proposer un thème est passif, en changer ne l’est pas.
agent:toolsctx.agent.registerTool — enregistrer des outils pour l'assistant de lecture.
service:networkctx.network.fetch — requêtes HTTP sortantes, via le client natif de l'application (pas de contrainte CORS).
service:llmctx.llm.ask— faire un appel de modèle unique en utilisant le compte configuré de l'utilisateur. Pas de fil, pas de mémoire, pas d'outils ; prend en charge la sortie JSON structuré via schema, ou la réception de texte en flux via onText.
service:clipboardctx.clipboard.writeText.

(reader:modes— modes de lecture guidés rendus par l'hôte — temporairement limité aux extensions de première partie livrées avec l'application jusqu'à ce que ce contrat privilégié se stabilise.)

Points de contribution

Actions de sélection

Entrées dans le menu de sélection du lecteur et le menu d'annotation. Le gestionnaire reçoit le texte sélectionné, sa plage CFI, le chapitre et le livre ; quand le lecteur peut le récupérer, context porte également les paragraphes de contexte autour de la sélection. À l'intérieur du lecteur, une action soit s'exécute silencieusement (retourne un toast), soit ouvre un dialogue (retourne une vue) — seulement ces deux résultats. Les actions asynchrones déclarant presentation: "dialog" font que l'hôte ouvre immédiatement un dialogue en état de chargement, et remplit le résultat dans la même requête quand run se termine. Les actions de type dictionnaire peuvent déclarer role: "lookup" : l'hôte routera la commande clavier « Chercher » existante vers cette action d'extension, au lieu de maintenir un second chemin de recherche intégré.

ctx.ui.registerSelectionAction({
  id: "save-quote",
  title: "Enregistrer la citation",
  icon: "quotes",
  presentation: "dialog",
  run: (input) => {
    // input: { text, context?, cfiRange, chapterHref, book, source }
    return { toast: "Citation enregistrée." };
  },
});

Actions d'en-tête

Un bouton icône dans l'en-tête. Sur la surface du lecteur, la vue s'ouvre en panneau ancré ; sur la bibliothèque, elle s'ouvre selon presentation en panneau ou en page complète. Le lecteur ne permet jamais d'interruption en pleine page.

ctx.ui.registerHeaderAction({
  id: "reading-report",
  title: "Bilan de lecture",
  icon: "chart-line-up",
  surface: "shelf",
  presentation: "page",
  view: async () => ({
    kind: "markdown",
    title: "Cette semaine",
    markdown: "Vous avez lu **4h 12m** sur 3 livres.",
  }),
});

Commandes

Une entrée dans la palette de commandes. Toutes les actions d'extension apparaissent automatiquement dans la palette ; les commandes explicites servent pour les actions sans bouton.

ctx.ui.registerCommand({
  id: "sync-now",
  title: "Anki Sync: synchroniser maintenant",
  run: async () => ({ toast: "Synchronisé." }),
});

Outils de l'assistant

Outils que l'assistant de lecture peut appeler pendant une conversation (nécessite la permission agent:tools). parameters est un schéma JSON ordinaire décrivant l'objet de paramètres ; les outils sans paramètres peuvent l'omettre. Les outils sont préfixés par espace de noms en plugin_<pluginId>_<name> avant d'être envoyés au modèle, et les invocations apparaissent dans la conversation en tant qu'étapes d'outil pour l'utilisateur.

ctx.agent?.registerTool({
  name: "search_deck",
  label: "Recherche de votre paquet Anki",
  description: "Rechercher un terme dans la collection Anki de l'utilisateur.",
  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();
  },
});

Fournisseurs de voix de lecture à haute voix

ctx.audio.registerVoiceProvider connecte un moteur de synthèse vocale à la fonction de lecture à haute voix de la page de lecture. L'extension synthétise uniquement le texte en octets audio encodés (mp3/wav — tout ce que la webview peut décoder) ; la lecture, la progression phrase par phrase, la pré-récupération et le surlignage de suivi sont tous gérés par l'application. L'enregistrement lui-même ne nécessite pas de permission — les capacités requises pour la synthèse (réseau, clés) sont déjà protégées par les propres autres permissions de l'extension.

ctx.audio.registerVoiceProvider({
  id: "voices",
  label: "Mon TTS",
  listVoices: () => [{ id: "default", label: "Mon TTS · chaleureux" }],
  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();
  },
});

Les voix enregistrées sont automatiquement adoptées — activer votre extension est le choix, l'hôte ne maintient pas de sélecteur distinct ; si une seule synthèse échoue, elle revient à la voix système, la lecture se dégrade seulement, sans interruption. Les changements de paramètres d'extension déclenchent une réénumération des voix.

Tâches planifiées

Le manifest déclare les tâches périodiques, activate lie le travail réel. L'application exécute au moins toutes les everyMinutes minutes (minimum 15) pendant qu'elle est ouverte, et rattrape une exécution manquée au démarrage — jamais de promesse de moment précis, et aucune exécution si l'application est fermée. Les exécutions simultanées de la même tâche sont ignorées ; un échec attend simplement la période suivante.

// manifest.json
"schedules": [{ "id": "refresh", "label": "Actualiser les flux", "everyMinutes": 60 }]

// main.js
ctx.schedule.on("refresh", async () => {
  // récupérer, comparer, réécrire via les APIs de domaine
});

Thèmes et polices groupées

Avec ui:themes déclaré, le manifest peut déclarer des thèmes pour deux points de montage indépendants — l'interface de l'application et les pages de livres — et grouper des fichiers de polices distribués avec le dossier de l'extension. Ces contributions sont purement des données : l'application valide chaque valeur et génère tout le CSS elle-même, et rien ne prend effet avant que l'utilisateur ne sélectionne le thème dans « Réglages → Apparence » ou le sélecteur de couleur de page du lecteur. Les extensions purement thème ont juste besoin de export default { activate() {} } dans main.js.

{
  "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": "Crépuscule", "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 le thème lit clair ou sombre. Pilote color-scheme, les valeurs par défaut de polarité claires/sombres dont les tokens d'application non remplacés héritent, et la résolution de la couleur de page « Auto » du lecteur pendant que le thème est actif.
  • app — remplace le vocabulaire de tokens fixes de l'application (toile, hiérarchie de texte, surfaces, remplissage, bordures — voir PluginAppThemeTokens dans la déclaration de type). Les tokens non remplacés gardent les propres valeurs de polarité correspondantes.
  • reader — une palette à six couleurs (les six obligatoires) identique aux couleurs de page intégrées, plus un préréglage de typographie optionnel : appliqué une fois au moment où l'utilisateur sélectionne le thème, puis l'utilisateur peut ajuster librement.
  • fonts.woff2/.woff/.ttf/.otf servis directement depuis le dossier de l'extension ; chaque police apparaît dans le sélecteur de polices du lecteur pendant que l'extension est activée. Les thèmes référencent leurs propres polices avec plugin:<fontId>. Les extensions soumises au marché doivent lister les fichiers de police dans le champ files de l'entrée de registre.
  • Les valeurs de couleur sont validées par syntaxe stricte — hex pur ou rgb()/rgba()/hsl()/hsla() ; les mots-clés, var() et url() sont rejetés.

Vues

Les extensions déclarent des arbres de composants hôtes, l'application rend toutes les primitives visuelles et contrôles ; les extensions ne peuvent pas fournir de JSX, HTML, CSS ou className.

  • markdown — une chaîne markdown, rendue par l'application.
  • list — l'hôte fournit la recherche avec debounce fixe, keywords, accessories et état vide ; timeline fournit un filtre aujourd'hui / cette semaine / ce mois / tout et un regroupement de dates locales, les éléments peuvent utiliser presentation: "dialog" pour ouvrir une vue de retour au-dessus de la liste, au lieu de descendre dans une sous-page.
  • form — text, textarea, number, time, select, choice, checkbox, toggle utilisant la bibliothèque de composants ReadAware, plus onSubmit ; ce dernier reçoit les valeurs du formulaire, peut retourner une vue de résultat ou des erreurs de champ.
  • detail — contenu principal, métadonnées et actions de style Raycast ; l'hôte rend les actions comme boutons icônes à droite du titre du contenu, et affiche discrètement les métadonnées source, date et tags au bas du contenu.
  • blocks— typography hôte, markdown, dictionnaire, métadonnées, citation, actions, métrique, progression, tags, avertissement, section, group et columns responsive. Les colonnes exposent uniquement le weight relatif, les niveaux d'espacement, les niveaux de largeur minimale et l'alignement sémantique, le CSS réel et le wrapping restent au système de design ; toutes les déclarations sont validées au runtime et la profondeur d'imbrication est limitée.

Les gestionnaires (run, onSelect, onSubmit) retournent tous la même forme de résultat :

  • Ne retourner rien — l'UI reste comme elle est ;
  • { toast: "…" } — un message bref ;
  • { view } — ouvrir une UI, ou pousser une nouvelle couche de vue par-dessus ;
  • { view, navigation: "replace" | "reset" }— remplacer la vue actuelle, ou revenir à un nouvel arbre racine ;
  • { close: true } — fermer l'UI (combinable avec toast) ;
  • { fieldErrors }— depuis la soumission de formulaire : rester dans le formulaire et afficher les erreurs sous les champs.

Le travail asynchrone n'est pas remarquable : retournez une promesse, l'application affiche un état de chargement. Les icônes sont choisies par nom dans la collection Phosphor sélectionnée par l'application — pas de SVG personnalisé pris en charge.

Données de domaine

Chaque espace de noms de domaine accordé fournit des lectures, des abonnements aux événements canoniques, et (avec permission d'écriture) des commandes. Vue d'ensemble :

  • ctx.shelf.bookslist(), get(id), getToc(id), getChapterText(id, index); écritures : import, editMetadata, setStarred, setFinished, remove, plus les fournisseurs de contenu (voir ci-dessous).
  • ctx.shelf.collectionslist(), booksIn(id); écritures : create, rename, remove, assignBooks(bookIds, collectionId | null).
  • ctx.shelf.statsforBook(bookId), list(), overview()(position de lecture, état de lecture et durée de lecture réelle ; lecture seule pour tout acteur).
  • ctx.annotations list({ bookId?, kind?, query? }) retourne une union discriminée de surlignages, notes et questions ; écritures : createHighlight, recolorHighlight, removeHighlight, createNote, updateNote, removeNote.
  • ctx.conversationsgetBookThread(bookId), listThreads(), getThread(id) ; abonnez-vous via on (aiConversation.started, aiMessage.appended, aiMessage.removed, aiConversation.cleared).

Événements

Deux types d'événements, délibérément séparés. Les événements de domaine sont les faits enregistrés par l'application ; abonnez-vous par domaine, utilisez des noms canoniques, nécessite la permission de lecture de ce domaine. Chaque livraison est de la forme { type, payload, createdAt, origin } — origin montre quel acteur logiciel a produit ce fait (user, agent, system, ou plugin:<id>).

ctx.annotations?.on("highlight.created", ({ payload, origin }) => {
  // payload: { highlightId, bookId, text, color?, … }
});
ctx.shelf?.on("book.removed", ({ payload }) => { /* { bookId } */ });

Les faits de session décrivent ce qui se passe à l'écran en ce moment. Ils n'entrent jamais dans le journal d'événements, et ne nécessitent aucune permission : ctx.session.on(event, handler).

Événement de sessionCharge utile
book-opened{ book: { id, title, author? } }
book-closed{ bookId }
chapter-changed{ bookId, chapterHref }
reading-progress{ bookId, fraction } — déclenché lors du tournage de page, fraction dans 0..1

Fournisseurs de contenu et livres virtuels

Avec shelf:write déclaré, les extensions peuvent placer de vrais livres sur la bibliothèque. import accepte des octets de fichier. Les fournisseurs de contenu contournent complètement les fichiers : enregistrez un fournisseur, ajoutez des livres virtuels liés à celui-ci, et fournissez des chapitres HTML à la demande lorsque le livre est ouvert. Le lecteur les pagine, annote, enregistre la progression comme tout livre — « lire un flux RSS comme un livre » est exactement ainsi implémenté.

ctx.shelf?.books.write?.registerContentProvider({
  id: "rss",
  async load(key) {
    const feed = await fetchFeed(key); // votre code, via 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",
});

Stockage et paramètres

ctx.storage est un stockage clé-valeur par espace de noms persisté avec les données locales de l'application — get, set, remove. Si le manifest déclare un champ settings, l'application les rend comme la propre section de paramètres de l'extension, toutes les valeurs apparaissent comme un objet dans ctx.storage.get("settings"). L'assistant de lecture peut également voir et modifier ces paramètres (les champs marqués agentHidden lui sont invisibles). Trois capacités de champ vont au-delà du formulaire ordinaire :

  • visibleWhen: { field, equals } affiche un champ seulement lorsqu'un autre champ prend une valeur donnée. La valeur existante du champ masqué est conservée — un objet de paramètres unique peut stocker un ensemble de valeurs pour chaque variante (l'extension TTS stocke ainsi une voix pour chaque fournisseur).
  • select avec dynamicOptions: true résout les options au moment de l'exécution : liez une source dans activate avec ctx.settings.provideOptions(fieldId, async (values) => [...]). Quand la source ne peut pas fournir d'options (pas encore de clé configurée, point de terminaison inaccessible), le champ revient à la saisie de texte libre — la liste est une commodité, jamais une barrière.
  • kind: "secret" déclare un champ d'identifiants : l'application rend une entrée de mot de passe et écrit directement dans le stockage de secrets chiffré — l'id du champ est la clé que vous lisez avec ctx.secrets dans votre code — ne va jamais dans les paramètres en texte clair, ne va jamais dans le catalogue de l'assistant. La valeur existante n'est jamais ré-affichée ; le champ se présente comme état « configuré » avec une entrée de nettoyage.

Pour les données structurées, ctx.storage.collection(name) ouvre une collection de documents nommée — put / get / delete / list les documents individuels, les documents peuvent facultativement porter des informations de provenance bookId / anchor et peuvent être filtrés par celles-ci. La provenance est un index, pas une propriété : les documents survivent à la suppression du livre référencé ; et le cycle de vie de la collection appartient à l'extension (désinstaller efface). L'extension vocabulaire intégrée est entièrement construite sur cette couche.

Contexte résident

Toujours disponible, aucune permission nécessaire :

  • ctx.manifest, ctx.appVersion, ctx.locale (balise de langue BCP-47 actuelle de l'interface de l'application — lire au moment de l'utilisation, elle change avec le réglage de langue en temps réel) ;
  • ctx.ui.showToast(message) ;
  • ctx.ui.exportFile({ filename, content, mimeType? })— ouvre le flux de sauvegarde de l'hôte, exporte du texte généré (CSV, JSON, Markdown) ou des octets binaires ;
  • ctx.secrets — stockage d'identifiants chiffrés isolé par espace de noms de l'extension (jetons API, etc.) ; stocké en dehors de SQLite et des sauvegardes, survit à la désinstallation ;
  • ctx.session.on(…) — les faits de session ci-dessus ;
  • ctx.reader.openBook(bookId) et ctx.reader.goTo({ bookId?, cfi?, href? })— navigation dans le lecteur (contrôle visible de l'utilisateur, n'expose pas de données).

Stabilité

Ceci est le contrat v2, livré avec l'application 0.3.0 — une refonte disruptive intentionnelle qui dérive toute la surface d'extension du modèle de domaine (les manifests v1 échoueront à l'installation avec des messages d'erreur lisibles). À partir de maintenant, l'API ne fait que grandir additivement : nouveaux domaines, nouveaux noms d'événements, nouveaux types de blocs — les thèmes déclaratifs (ui:themes) en sont le premier ajout. Les modifications disruptives du contenu déjà documenté sur cette page seront traitées comme des bugs. Toute extension dépendant d'une capacité plus récente ajoutée devrait déclarer minAppVersion.