这页只讲怎么写。Haiku 不托管、不分发任何内容型插件 —— 插件包从哪来、装给谁,是你自己的事。 下面所有字段与函数都以三个真实插件样例和 CJRT 的 ABI 文档核对过。
目录
一个插件是 plugin.js 加 manifest.json 的分发单位,安装到
<AppSupport>/cjrt_plugins/<id>/。它是代码。
「来源」是用户在界面上添加、登录、启停、排序、删除的那个实体。它是实例。
来源 id 就是已安装的插件 id —— 同一份 plugin.js 以不同 id 安装两次,
就是两个凭据完全独立的来源,宿主侧一行代码都不用改。
标记为音乐库的插件可以这样派生多个来源;其余插件是一个插件对应一个来源。
顶层描述插件包本身,music 段描述它作为音乐插件的行为。
顶层字段
music 段
样例 · 音乐库插件
{
"id": "subsonic.music",
"name": "Subsonic",
"version": "1.1.0",
"entry": "plugin.js",
"logo": "logo.png",
"engine": { "minCoreVersion": "0.1.0" },
"music": {
"abiVersion": 1,
"kind": "library",
"provider": "subsonic",
"displayName": "Subsonic",
"capabilities": ["search", "track", "playback", "lyrics",
"playlist", "auth", "recommend", "home", "cover"],
"auth": { "required": true, "managed": true, "methods": ["form"] },
"idSchemes": ["subsonic:song", "subsonic:playlist", "subsonic:album"],
"instance": {
"keyFields": ["baseUrl", "username"],
"fields": [
{ "key": "baseUrl", "label": "服务器地址", "type": "url",
"required": true, "placeholder": "http://192.168.1.10:4533" },
{ "key": "username", "label": "用户名", "type": "text", "required": true },
{ "key": "password", "label": "密码", "type": "password", "required": true }
]
}
}
} 样例 · 刮削插件
{
"id": "kugou.scraper",
"name": "KuGou Scraper",
"version": "1.0.0",
"entry": "plugin.js",
"logo": "logo.png",
"engine": { "minCoreVersion": "0.1.0" },
"music": {
"abiVersion": 1,
"provider": "kugou",
"displayName": "酷狗刮削",
"kind": "scraper",
"capabilities": ["scrapeSearch", "scrapeLyrics"]
}
} kind 决定这个插件在产品里的身份,缺省是 provider。
capabilities
界面按能力过滤:没声明 search 的插件不会出现在搜索的来源切换里,没声明 lyrics 的会被跳过,直接走应用层的多来源歌词查找。
按你声明的能力导出对应函数即可,没声明的不必实现。下表是三个真实插件里实际出现的导出面。 函数签名以 CJRT 的 ABI 文档为准,本表给出的是用途。
出错时抛一个 MusicPluginError:把结构化信息塞进 message 的 JSON 里,
宿主侧会归一化成统一的错误类型。
export function musicError(code, message, extra = {}) {
const err = new Error(JSON.stringify({ code, message, ...extra }));
err.name = 'MusicPluginError';
return err;
}
// 用法
throw musicError('AUTH_REQUIRED', '请先配置 Subsonic 服务器');
常用错误码:AUTH_REQUIRED(需要登录或凭据缺失)、
NOT_FOUND(内容不存在或为空)。单个插件抛错不会拖垮整个应用,
只是该来源在这次调用里缺席。
插件自管凭据,用标准的 localStorage,落盘在自己安装目录下的
storage.json。升级插件时这个文件会被保留,用户不必重新登录。
务必照做
所有键都要带你自己的前缀。 规格文档说 storage 按安装目录天然隔离,
但实测各插件的 storage.json 会互相串键。
subsonic 插件因此把所有键统一加了 ss: 前缀。用裸键名迟早撞车。
// 键一律带自己的前缀。不要用裸键名。
const CRED_KEY = 'ss:credential';
const raw = localStorage.getItem(CRED_KEY);
localStorage.setItem(CRED_KEY, JSON.stringify(cred)); 最常见的坑
改了 plugin.js,必须把 manifest 的 version 往上抬。
宿主是按版本号比对来决定要不要覆写已装文件的。版本没变,已装用户就永远拿不到你的新代码 ——
你本地测得好好的,用户那边毫无变化。
版本变化时,宿主覆写 manifest.json 与 plugin.js,
但保留 storage.json,所以用户的登录状态不受影响。
一个插件包就是一个目录,里面三个文件:
<你的插件 id>/
manifest.json
plugin.js
logo.png
用户在设置的来源管理里通过本地目录或 zip,或者一个 URL 导入。
导入时会校验 manifest.id 与安装用的 id 是否一致。
导入插件意味着在你的设备上运行第三方代码,Haiku 会就此提示用户。 这是插件机制的固有前提,写清楚你的插件做什么、连哪里,对用户和你都好。