外掛 API 參考
外掛是一個資料夾,裡面有一份 manifest.json 和一個 JavaScript 模組。本頁就是編寫契約;同一份契約以 TypeScript 宣告檔案(types/plugin-api.d.ts)的形式隨外掛市集儲存庫一起發佈,編輯器可以對下文的一切自動補全。
結構
my-plugin/
manifest.json
main.js # 單個自包含的 ES modulemain.js 預設匯出一個生命週期物件。外掛能觸及的一切都來自傳給 activate 的上下文;每個 register* 與 on 呼叫都回傳一個 disposable,外掛被停用或解除安裝時由應用程式統一回收,因此 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": "Send looked-up words to Anki.",
"author": "you",
"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 | manifest 中宣告式的 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——使用使用者設定的帳號發起一次性模型呼叫。沒有討論串、沒有記憶、沒有工具;支援以 schema 輸出結構化 JSON,或以 onText 串流接收文字。 |
service:clipboard | ctx.clipboard.writeText。 |
(reader:modes——宿主渲染的引導式閱讀模式——在這份特權契約穩定下來之前,暫時僅限隨應用程式內建的第一方外掛使用。)
貢獻點
選取動作
閱讀器選取選單與標註選單中的條目。處理函數會收到選取的文字、它的 CFI 範圍、所在章節和書籍;當閱讀器能夠恢復時,context 還帶有選取周圍的上下文段落。在閱讀器內,一個動作要麼靜默執行(回傳 toast),要麼開啟對話框(回傳檢視)——只有這兩種結果。 非同步動作宣告 presentation: "dialog" 後,宿主會立刻開啟 載入狀態對話框,並在 run 完成時把結果填入同一次請求。 字典類動作可以宣告 role: "lookup":宿主會把現有的 「查詢」鍵盤命令路由到該外掛動作,而不是維護第二條內建查詞路徑。
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." };
},
});頂欄動作
頂欄上的一個圖示按鈕。在閱讀器介面,檢視以錨定的彈出層開啟;在書架上,則依 presentation 以彈出層或完整頁面開啟。閱讀器永遠不允許整頁打斷。
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.",
}),
});命令
命令面板中的一個條目。所有外掛動作都會自動出現在面板裡;顯式命令用於那些沒有按鈕的動作。
ctx.ui.registerCommand({
id: "sync-now",
title: "Anki Sync: sync now",
run: async () => ({ toast: "Synced." }),
});助理工具
閱讀助理在對話中可以呼叫的工具(需要 agent:tools 權限)。parameters 是描述參數物件的普通 JSON Schema;無參數的工具可以省略。工具在送達模型之前會被命名空間化為 plugin_<pluginId>_<name>,呼叫過程會以工具步驟的形式在對話中對使用者可見。
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();
},
});朗讀聲音供應商
ctx.audio.registerVoiceProvider 把一個文字轉語音引擎接進閱讀頁的朗讀功能。外掛只負責把文字變成編碼後的音訊位元組(mp3/wav——webview 能解碼的都行);播放、逐句推進、預先擷取與跟讀標示全部由應用程式負責。註冊本身不需要權限——合成所需的能力(網路、金鑰)已由外掛自己的其他權限門控。
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();
},
});註冊的聲音會被自動採用——使用者啟用你的外掛即是選擇,宿主不再另設選擇器;某一句合成失敗時會退回系統語音,朗讀只會降級、不會中斷。外掛設定變化時會重新列舉聲音。
排程任務
manifest 負責宣告週期任務,activate 負責繫結實際工作。應用程式在開啟期間至少每 everyMinutes 分鐘(下限 15)執行一次,逾期未跑的會在啟動後補一次——從不承諾精確時刻,應用程式關閉時也不會執行。同一任務的重疊執行會被跳過;失敗的一次只需等待下個週期。
// manifest.json
"schedules": [{ "id": "refresh", "label": "Refresh feeds", "everyMinutes": 60 }]
// main.js
ctx.schedule.on("refresh", async () => {
// 擷取、比對,經由領域 API 寫回
});主題與自帶字型
宣告 ui:themes 後,manifest 可以為兩個相互獨立的掛載點——應用程式介面與書頁——宣告主題,並附帶隨外掛資料夾分發的字型檔案。這類貢獻是純資料:應用程式校驗每一個值並自行產生全部 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": "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——主題讀起來偏亮還是偏暗。它驅動color-scheme、主題未覆寫的應用程式 token 所繼承的明暗預設值,以及主題生效期間閱讀器「自動」頁面顏色的解析。app——對應用程式固定 token 詞彙(畫布、文字層級、表面、填充、邊框——見型別宣告中的PluginAppThemeTokens)的覆寫。未覆寫的 token 保持對應明暗極性自己的值。reader——與內建頁面顏色同一套的六色調色盤(六色缺一不可),外加一個可選的排版預設:在使用者選取主題的那一刻一次性套用,之後使用者可以隨意調整。fonts——.woff2/.woff/.ttf/.otf字型直接從外掛資料夾提供服務;外掛啟用期間,每個字體都會出現在閱讀器的字體選擇器裡。主題以plugin:<fontId>引用自己的字型。上架市集的外掛必須把字型檔案列進 registry 條目的files。- 顏色值按嚴格語法校驗——純 hex 或
rgb()/rgba()/hsl()/hsla();關鍵字、var()與url()一律拒絕。
檢視
外掛宣告的是宿主元件樹,由應用程式渲染所有視覺原語和控制項;外掛不能提供 JSX、HTML、CSS 或 className。
markdown——一個 markdown 字串,由應用程式排版。list——宿主提供固定 debounce 的搜尋、keywords、 accessories 與空狀態;timeline提供今天/本週/本月/ 全部篩選和本機日期分組,條目可用presentation: "dialog"在清單上方開啟回傳檢視,而不是下鑽成子頁面。form——使用 ReadAware 元件庫的 text、textarea、number、time、select、choice、checkbox、toggle,加上onSubmit;後者接收表單值,可回傳結果檢視或欄位錯誤。detail——Raycast 式主內容、metadata 與宿主 actions;宿主把 actions 渲染成內容標題右側的圖示按鈕,把來源、日期和 tags 等 metadata 收進安靜的內容底部。blocks——宿主 typography、markdown、字典、metadata、引文、動作、指標、進度、標籤、提示、section、group 與響應式columns。columns 只開放相對 weight、間距檔位、最小寬度檔位和語義對齊,具體 CSS 與換行仍歸設計系統;所有宣告都會在執行時校驗並限制巢狀深度。
處理函數(run、onSelect、onSubmit)都回傳同一種結果形態:
- 什麼都不回傳——介面保持原樣;
{ toast: "…" }——一條短暫的提示;{ view }——開啟介面,或在其上推入一層新檢視;{ view, navigation: "replace" | "reset" }——替換目前檢視,或回到一棵新的根檢視;{ close: true }——關閉介面(可與toast組合);{ fieldErrors }——來自表單提交:停留在表單上,並在欄位下方顯示錯誤。
非同步工作不值一提:回傳一個 promise,應用程式會顯示載入狀態。圖示按名稱從應用程式精選的 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)。
| 工作階段事件 | 承載 |
|---|---|
book-opened | { book: { id, title, author? } } |
book-closed | { bookId } |
chapter-changed | { bookId, chapterHref } |
reading-progress | { bookId, fraction }——翻頁時觸發,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。如果 manifest 宣告了 settings 欄位,應用程式會把它們渲染成外掛自己的設定分類,所有值會以一個物件出現在 ctx.storage.get("settings")。閱讀助理也能查看和修改這些設定(標記 agentHidden 的欄位對它不可見)。有三種超出普通表單的欄位能力:
visibleWhen: { field, equals }讓欄位只在另一欄位取給定值時顯示。隱藏欄位的存量值會保留——一個設定物件即可按變體各存一套值(TTS 外掛正是這樣為每個供應商各記一個聲音)。select配合dynamicOptions: true可以在執行時解析選項:在activate裡用ctx.settings.provideOptions(fieldId, async (values) => [...])繫結來源。來源給不出選項時(還沒設定金鑰、端點不可達),欄位會退回自由文字輸入——清單是便利,絕不是門檻。kind: "secret"宣告一個憑證欄位:應用程式渲染密碼輸入框並直寫加密的 secret store——欄位 id 就是你程式碼裡ctx.secrets讀回的鍵名——絕不進明文設定,也不進助理的目錄。存量值從不回顯;欄位以「已設定」狀態展示,並提供清除入口。
對於結構化資料,ctx.storage.collection(name) 會開啟一個具名的文件集合——對逐條文件記錄進行 put / get / delete / list,記錄可選攜帶 bookId / anchor 出處資訊,並可據此篩選。出處是索引而非所有權:被引用的書刪除後,文件依然存在;而集合的生命週期歸屬於外掛(解除安裝即清空)。內建的詞彙表外掛正是完全建構在這一層之上。
常駐上下文
始終可用,無需任何權限:
ctx.manifest、ctx.appVersion、ctx.locale(應用程式介面目前的 BCP-47 語言標籤——用時再讀,它隨語言設定即時變化);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 的 manifest 會安裝失敗,並給出可讀的錯誤訊息)。自此 API 只做加法式增長:新的領域、新的事件名、新的區塊類型——宣告式主題(ui:themes)就是第一個這樣的新增。對本頁已記載內容的破壞性變更會被當作 bug 處理。任何依賴較新能力的外掛,請宣告 minAppVersion。