Plugin-API-Referenz
Ein Plugin ist ein Ordner, der eine manifest.json und ein JavaScript-Modul enthält. Diese Seite ist der Autorenvertrag; derselbe Vertrag wird als TypeScript-Deklarationsdatei (types/plugin-api.d.ts) im Marktplatz-Repository ausgeliefert, sodass Editoren alles unten automatisch vervollständigen.
Anatomie
my-plugin/
manifest.json
main.js # ein in sich geschlossenes ES-Modulmain.js exportiert standardmäßig ein Lebenszyklus-Objekt. Alles, was ein Plugin erreichen kann, kommt durch den Kontext, der an activate übergeben wird; jeder register*- und on-Aufruf gibt ein Disposable zurück, das die App zurückfordert, wenn das Plugin deaktiviert oder deinstalliert wird, sodass deactivate nur die eigenen externen Ressourcen des Plugins freigeben muss.
export default {
activate(ctx) {
// Beiträge über ctx registrieren
},
deactivate() {
// optional: Sockets schließen, Warteschlangen leeren
},
};Aktivieren und Deaktivieren wirken sich sofort aus — kein App-Neustart. Schreib gern in TypeScript (empfohlen; siehe Veröffentlichen) — was die App lädt, ist immer die gebaute main.js.
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"
}| Feld | Bedeutung |
|---|---|
id | Kleinbuchstaben, Ziffern, Bindestriche (max. 64). Muss dem Ordnernamen entsprechen; legt den Namensraum für Speicher und Werkzeuge des Plugins fest. |
name, version | Angezeigt in Einstellungen → Plugins und im Marktplatz. |
minAppVersion | Niedrigste App-Version, die das Plugin unterstützt. Dieser Vertrag erfordert 0.3.0 oder neuer. |
permissions | Was das Plugin benutzt (Tabelle unten). Wird dir vor der Installation angezeigt. |
main | Einstiegsmodul relativ zum Ordner; Standardwert ist main.js. |
settings | Optionale deklarative Einstellungen (dieselben Feldformen wie Formularansichten, plus secret). Die App rendert sie als eigene Sektion des Plugins in den Einstellungen und persistiert die Werte als ein Objekt unter dem Speicherschlüssel settings — siehe Speicher und Einstellungen. |
schedules | Optionale wiederkehrende Aufgaben, deklariert, damit du sie vor der Installation siehst — siehe Geplante Arbeit. |
themes, fonts | Optionale deklarative Themes und gebündelte Schriften (erfordert ui:themes) — siehe Themes und gebündelte Schriften. |
Das Domänenmodell
Die Datenoberfläche wird vom Domänenmodell der App abgeleitet, anstatt daneben erstellt zu werden. Jede Domäne — shelf (die gesamte Bibliotheksverwaltung: Bücher, Sammlungen, Lesestatistiken), annotations, conversations — ist ein Namensraum auf ctx, der drei Dinge bereitstellt:
- Lesevorgänge — die Lesemodelle der Domäne (was die eigenen Oberflächen der App rendern);
- Schreibvorgänge — Befehle unter
.write, die genau die Event-Verben der Domäne spiegeln und über den eigenen event-sourced Schreibpfad der App gehen, gestempelt mitplugin:<id>im Event-Log, sodass jeder Plugin-Schreibvorgang zurechenbar ist; - Abonnements —
.on(event, handler)über die Events der Domäne unter ihren kanonischen Namen (book.starred,highlight.created, …) — das gleiche Vokabular, das die App selbst aufzeichnet.
Berechtigungen folgen derselben Form: <domain>:read / <domain>:write, und innerhalb einer Domäne impliziert write read. Gerätlokaler Zustand (View-Präferenzen, Reader-Erscheinungsbild, Sync-Interna) und freies Rendering sind bewusst keine Plugin-Oberfläche — UI geht durch die deklarativen Ansichten unten.
Berechtigungen
Fähigkeitsgruppen auf ctx fehlen schlicht, solange ihre Berechtigung nicht deklariert ist — Gating auf API-Ebene gegen versehentliche Übergriffe. Namensraum-Speicher, UI-Beiträge, Session-Events und Reader-Navigation sind keine Berechtigungen; jedes Plugin hat sie.
| Berechtigung | Gewährt |
|---|---|
shelf:read | ctx.shelf — Bücher (inkl. Inhaltsverzeichnis und Kapiteltext eines Buches), Sammlungen und Mitgliedschaft, sowie Lesestatistiken (stats.forBook / stats.list / stats.overview — Statistiken haben keine Schreibseite: ihre Events sind aufgezeichnete Fakten der Reader-Aktivität, keine Nutzerbefehle). |
shelf:write | ctx.shelf.books.write — Dateien importieren, Metadaten bearbeiten, mit Stern markieren, als beendet markieren, entfernen; Inhaltsanbieter und virtuelle Bücher. ctx.shelf.collections.write — erstellen, umbenennen, entfernen, Bücher zuweisen. |
annotations:read / annotations:write | ctx.annotations — Markierungen, Notizen und gestellte Fragen; erstellen, umfärben, bearbeiten und entfernen von Markierungen und Notizen (Fragen sind vom Agenten geschrieben, schreibgeschützt). |
conversations:read | ctx.conversations — KI-Threads pro Buch und globale Threads (schreibgeschützt). |
ui:themes | Die deklarativen themes / fonts Manifest-Felder (unten) — App- und Reader-Themes mit gebündelten Schriften. Der einzige UI-Beitrag hinter einer Berechtigung: Er hat visuelle Autorität über die gesamte App, deshalb muss er bei der Installation offen zur Zustimmung stehen. |
ui:appearance | ctx.appearance — alle Themes auflisten, die beide Oberflächen anbieten, das aktuelle Erscheinungsbild lesen und App-Theme oder Seitenfarbe umschalten. Bewusst getrennt von ui:themes: ein Theme anzubieten ist passiv, eines umzuschalten nicht. |
agent:tools | ctx.agent.registerTool — Werkzeuge für den Lese-Assistenten. |
service:network | ctx.network.fetch — ausgehende HTTP über den nativen Client der App (keine CORS-Einschränkungen). |
service:llm | ctx.llm.ask — einmalige Modellanrufe über das konfigurierte KI-Konto des Nutzers. Kein Thread, kein Gedächtnis, keine Werkzeuge; unterstützt strukturierte JSON-Ausgabe über schema und Streaming über onText. |
service:clipboard | ctx.clipboard.writeText. |
(reader:modes — host-gerenderte geführte Lesemodi — ist derzeit für die gebündelten First-Party-Plugins reserviert, während dieser privilegierte Vertrag sich festigt.)
Contributions
Auswahlaktionen
Einträge in den Auswahl- und Annotationsmenüs des Readers. Der Handler erhält den ausgewählten Text, seinen CFI-Bereich, das Kapitel und das Buch. Wenn verfügbar, enthält context die umgebende Passage. Innerhalb des Readers führt eine Aktion entweder still aus (gibt einen Toast zurück) oder öffnet einen Dialog (gibt eine Ansicht zurück) — das sind die einzigen beiden Ergebnisse. Deklariere presentation: "dialog", wenn der Handler asynchron ist: Der Host öffnet sofort seine Ladeansicht und füllt sie mit dem Ergebnis, sobald run zurückkehrt. Eine Wörterbuch-artige Aktion kann role: "lookup" deklarieren; der Host leitet dann den bestehenden Tastaturbefehl fürs Nachschlagen an diese Plugin-Aktion um, statt einen zweiten eingebauten Nachschlage-Pfad zu pflegen.
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." };
},
});Kopfzeilenaktionen
Ein Icon-Button auf einer oberen Leiste. Auf der Reader-Oberfläche öffnet die Ansicht als verankertes Popover; auf dem Regal öffnet sie als Popover oder volle Seite, je nach presentation. Der Reader erlaubt niemals ganzseitige Unterbrechungen.
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.",
}),
});Befehle
Ein Befehlspaletten-Eintrag. Alle Plugin-Aktionen erscheinen auch automatisch in der Palette; explizite Befehle sind für Aktionen ohne Button.
ctx.ui.registerCommand({
id: "sync-now",
title: "Anki Sync: sync now",
run: async () => ({ toast: "Synced." }),
});Agenten-Werkzeuge
Werkzeuge, die der Lese-Assistent während des Chats aufrufen kann (erfordert agent:tools). parameters ist einfaches JSON Schema für das Argument-Objekt; lass es weg für ein Werkzeug ohne Argumente. Werkzeuge bekommen den Namensraum plugin_<pluginId>_<name>, bevor sie das Modell erreichen, und Aufrufe sind im Chat als Werkzeug-Schritte sichtbar.
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();
},
});Stimmenanbieter
ctx.audio.registerVoiceProvider steckt eine Text-zu-Sprache-Engine in das Vorlesen des Readers. Das Plugin wandelt nur Text in kodierte Audio-Bytes um (mp3/wav — alles, was die Webview dekodiert); die App besitzt Wiedergabe, Satz-Taktung, Vorabruf und die Mitlauf-Hervorhebung. Die Registrierung braucht keine eigene Berechtigung — was immer der Anbieter zum Synthetisieren braucht (Netzwerk, Schlüssel), ist bereits durch seine anderen Berechtigungen gegated.
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();
},
});Eine registrierte Stimme wird automatisch übernommen — wer dein Plugin aktiviert, hat damit gewählt, es gibt keinen separaten hostseitigen Auswahldialog — und ein fehlgeschlagener Synthese-Aufruf fällt auf die Systemstimme für diesen Satz zurück, sodass das Lesen sich verschlechtert, anstatt still zu werden. Stimmen werden neu aufgelistet, wenn sich die Einstellungen des Plugins ändern.
Geplante Arbeit
Das Manifest deklariert wiederkehrende Aufgaben; activate bindet die Arbeit. Die App führt jeden Zeitplan MINDESTENS alle everyMinutes aus (Mindestwert 15), während sie geöffnet ist, mit einem Aufhol-Lauf kurz nach dem Start, wenn überfällig — niemals zu exakten Zeiten und niemals, während die App geschlossen ist. Überlappende Durchläufe eines Zeitplans werden übersprungen; ein fehlgeschlagener Lauf wartet einfach auf die nächste Kadenz.
// manifest.json
"schedules": [{ "id": "refresh", "label": "Refresh feeds", "everyMinutes": 60 }]
// main.js
ctx.schedule.on("refresh", async () => {
// abrufen, abgleichen, über die Domänen-APIs schreiben
});Themes und gebündelte Schriften
Mit ui:themes kann das Manifest Themes für zwei unabhängige Einhängepunkte deklarieren — das App-Chrome und die Buchseite — plus Schriftdateien, die im Plugin-Ordner ausgeliefert werden. Dieser Beitrag ist reine Daten: Die App validiert jeden Wert und generiert alle CSS selbst, und nichts gilt, bis du das Theme unter Einstellungen → Darstellung oder über die Seitenfarben-Einstellung des Readers wählst. Die main.js eines Nur-Theme-Plugins ist nur 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— ob das Theme hell oder dunkel wirkt. Steuertcolor-scheme, die Polaritäts-Standardwerte für App-Token, die das Theme ungesetzt lässt, und wie die Auto-Seitenfarbe des Readers aufgelöst wird, während das Theme aktiv ist.app— Überschreibungen auf dem festen Token-Vokabular der App (Canvas, Text-Stufen, Oberflächen, Füllungen, Rahmen — siehePluginAppThemeTokensin den Typings). Nicht gesetzte Token behalten die Werte der Polarität.reader— die gleiche Sechs-Farben-Palette, die die eingebauten Seitenfarben verwenden (alle sechs erforderlich), plus eine optionale Typografie-Voreinstellung, die einmal angewendet wird, wenn du das Theme wählst; danach kannst du alles anpassen.fonts—.woff2/.woff/.ttf/.otfFaces direkt aus dem Plugin-Ordner bereitgestellt; jede erscheint in der Schriftauswahl des Readers, solange das Plugin aktiviert ist. Ein Theme verweist auf seine eigenen Schriften alsplugin:<fontId>. Marktplatz-Plugins müssen Schriftdateien imfiles-Feld des Registry-Eintrags auflisten.- Farben werden gegen strenge Grammatiken validiert — einfaches Hex oder
rgb()/rgba()/hsl()/hsla(); Schlüsselwörter,var()undurl()werden abgelehnt.
Views
Plugins deklarieren einen Baum von Host-Komponenten; die App rendert jedes visuelle Primitiv und jedes Steuerelement. Plugins liefern niemals JSX, HTML, CSS oder Klassen.
markdown— ein Markdown-String, von der App gesetzt.list— durchsuchbare Host-Listen mit festem Debounce, Keywords, Accessories und Empty States.timelinefügt Heute / Diese Woche / Diesen Monat / Alle Filter und lokale Datumsgruppen hinzu; ein Element kannpresentation: "dialog"verwenden, um seine zurückgegebene Ansicht über der Liste zu zeigen, anstatt eine Unterseite zu pushen. Listen-Level-actionssind host-gerenderte Icon-Buttons; Timelines platzieren sie ganz rechts in der Tab-Reihe.form— Text-, Textarea-, Number-, Time-, Select-, Choice-, Checkbox- und Toggle-Steuerelemente aus der ReadAware-Komponentenbibliothek, plusonSubmit.detail— Raycast-artiger Primärinhalt, Metadaten und host-gerenderte Steuerelemente und Aktionen. Semantische Select-Steuerelemente bleiben beim Inhalts-Heading; Dialoge halten Herkunft, Daten und Tags in einer ruhigen Zeile darunter, während Aktionen neben dem Host-Schließen-Button in einem festen Footer sitzen.blocks— Host-Typografie, Markdown, Wörterbuchinhalt, Metadaten, Zitate, Aktionen, Metriken, Fortschritt, Tags, Alerts, Sections, Groups und responsivecolumns. Spalten bieten nur begrenzte Gewichte, Abstände, Mindestbreiten-Voreinstellungen und semantische Ausrichtung an. Exaktes CSS und Umbruch bleiben im Design-System; Deklarationen werden zur Laufzeit validiert und die Verschachtelung ist begrenzt.
Handler (run, onSelect, onSubmit) geben alle dieselbe Ergebnisform zurück:
- nichts — die Oberfläche bleibt, wie sie ist;
{ toast: "…" }— ein vorübergehender Hinweis;{ view }— öffnen oder auf die Oberfläche pushen;{ view, navigation: "replace" | "reset" }— die aktuelle Ansicht ersetzen oder zu einer neuen Root-Ansicht zurückkehren;{ close: true }— die Oberfläche schließen (komponierbar mittoast);{ fieldErrors }— von einem Formular-Submit: auf dem Formular bleiben und Fehler unter den Feldern zeigen.
Asynchrone Arbeit ist unkompliziert: Gib ein Promise zurück, und die App zeigt den Ladezustand. Icons werden per Name aus dem kuratierten Phosphor-Set der App gewählt — kein eigenes SVG.
Domänendaten
Jeder gewährte Domänen-Namensraum bietet Lesevorgänge, kanonische Event-Abonnements und (mit der Schreibberechtigung) Befehle. Kurz gefasst:
ctx.shelf.books—list(),get(id),getToc(id),getChapterText(id, index); Schreibvorgänge:import,editMetadata,setStarred,setFinished,remove, plus Inhaltsanbieter (unten).ctx.shelf.collections—list(),booksIn(id); Schreibvorgänge:create,rename,remove,assignBooks(bookIds, collectionId | null).ctx.shelf.stats—forBook(bookId),list(),overview()(Positionen, Status und aktive Lesezeit; schreibgeschützt für jeden Akteur).ctx.annotations—list({ bookId?, kind?, query? })gibt eine diskriminierte Union von Markierungen, Notizen und Fragen zurück; Schreibvorgänge:createHighlight,recolorHighlight,removeHighlight,createNote,updateNote,removeNote.ctx.conversations—getBookThread(bookId),listThreads(),getThread(id); abonnieren überon(aiConversation.started,aiMessage.appended,aiMessage.removed,aiConversation.cleared).
Events
Zwei Klassen, bewusst getrennt. Domänen-Events sind die Fakten, die die App aufzeichnet; abonniere sie pro Domäne, unter kanonischen Namen, mit der Leseberechtigung der Domäne. Jede Zustellung ist { type, payload, createdAt, origin } — origin gibt an, welcher Software-Akteur das Faktum produziert hat (user, agent, system oder plugin:<id>).
ctx.annotations?.on("highlight.created", ({ payload, origin }) => {
// payload: { highlightId, bookId, text, color?, … }
});
ctx.shelf?.on("book.removed", ({ payload }) => { /* { bookId } */ });
Session-Fakten beschreiben, was gerade auf dem Bildschirm zu sehen ist. Sie gelangen niemals ins Event-Log und benötigen keine Berechtigung: ctx.session.on(event, handler).
| Session-Event | Payload |
|---|---|
book-opened | { book: { id, title, author? } } |
book-closed | { bookId } |
chapter-changed | { bookId, chapterHref } |
reading-progress | { bookId, fraction } — feuert bei Seitenwechseln, Anteil 0..1 |
Inhaltsanbieter und virtuelle Bücher
Mit shelf:write kann ein Plugin echte Bücher auf das Regal stellen. import nimmt die Bytes einer Datei. Inhaltsanbieter überspringen die Datei vollständig: Registriere einen Anbieter, füge virtuelle Bücher hinzu, die an ihn gebunden sind, und liefere HTML-Abschnitte, sobald das Buch geöffnet wird. Der Reader paginiert, annotiert und verfolgt den Fortschritt auf ihnen wie bei jedem Buch — ein RSS-Feed als Buch ist genau das.
ctx.shelf?.books.write?.registerContentProvider({
id: "rss",
async load(key) {
const feed = await fetchFeed(key); // dein Code, über 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",
});Speicher und Einstellungen
ctx.storage ist ein Key-Value-Speicher im eigenen Namensraum, persistiert mit den lokalen Daten der App — get, set, remove. Wenn das Manifest settings-Felder deklariert, rendert die App sie als eigene Sektion des Plugins in den Einstellungen und die Werte kommen bei ctx.storage.get("settings") als ein Objekt an. Der Lese-Assistent kann diese Einstellungen auch einsehen und ändern (Felder, die mit agentHidden markiert sind, bleiben außer Sicht). Drei Feldfähigkeiten gehen über ein einfaches Formular hinaus:
visibleWhen: { field, equals }zeigt ein Feld nur, während ein anderes Feld einen der gegebenen Werte hält. Versteckte Felder behalten ihre gespeicherten Werte — ein Einstellungs-Objekt kann einen Wert pro Variante tragen (das TTS-Plugin hält auf diese Weise eine Stimme pro Anbieter).- Ein
selectmitdynamicOptions: truelöst seine Optionen zur Laufzeit auf: Binde die Quelle inactivatemitctx.settings.provideOptions(fieldId, async (values) => [...]). Wenn die Quelle nichts liefert (noch keine Anmeldedaten, Endpunkt nicht erreichbar), fällt das Feld auf freie Texteingabe zurück — die Auflistung ist ein Komfort, nie eine Hürde. kind: "secret"deklariert eine Anmeldeinformation: Die App rendert ein Passwort-Eingabefeld, das in den verschlüsselten Secret-Store schreibt — die Feld-ID IST derctx.secrets-Schlüssel, den dein Code ausliest — niemals in einfache Einstellungen und niemals in den Katalog des Assistenten. Der gespeicherte Wert wird niemals angezeigt; das Feld zeigt einen konfigurierten Zustand und eine Möglichkeit zum Leeren.
Für strukturierte Daten öffnet ctx.storage.collection(name) eine benannte Dokumenten-Sammlung — put / get / delete / list über pro-Dokument-Datensätze, mit optionaler bookId / anchor-Herkunft, nach der du filtern kannst. Herkunft ist ein Index, kein Eigentum: Dokumente überleben die Löschung des referenzierten Buches, und der Lebenszyklus der Sammlung gehört dem Plugin (Deinstallation löscht sie). Das eingebaute Wörterbuch-Plugin und seine gespeicherte-Wort-Timeline sind vollständig auf dieser Ebene aufgebaut.
Umgebender Kontext
Immer verfügbar, keine Berechtigung benötigt:
ctx.manifest,ctx.appVersion,ctx.locale(das aktuelle BCP-47-Locale der App-UI — lies es zum Zeitpunkt der Verwendung, es folgt der Spracheinstellung live);ctx.ui.showToast(message);ctx.ui.exportFile({ filename, content, mimeType? })öffnet den Speichern-Dialog des Hosts für generierten Text (CSV, JSON, Markdown) oder binäre Bytes;ctx.secrets— verschlüsselter Speicher für Anmeldedaten, pro Plugin unter eigenem Namensraum (API-Token und ähnliches); lebt außerhalb von SQLite und Backups und überlebt die Deinstallation;ctx.session.on(…)— die Session-Fakten oben;ctx.reader.openBook(bookId)undctx.reader.goTo({ bookId?, cfi?, href? })— Reader-Navigation (für dich sichtbare Steuerung, keine Datenoffenlegung).
Stabilität
Dies ist Vertrag v2, ausgeliefert in App 0.3.0 — ein bewusster Neuaufbau mit Brüchen, der die gesamte Oberfläche aus dem Domänenmodell ableitet (v1-Manifeste scheitern bei der Installation mit einer lesbaren Fehlermeldung). Von hier wächst die API additiv: neue Domänen, neue Event-Namen, neue Block-Arten — deklarative Themes (ui:themes) sind die erste solche Ergänzung. Brechende Änderungen an dem, was hier dokumentiert ist, werden als Bugs behandelt. Deklariere minAppVersion für alles, was von einer neueren Ergänzung abhängt.