Haiku · 插件编写规范 CAT. 920 返回首页

写一个 Haiku 能装的插件

这页只讲怎么写。Haiku 不托管、不分发任何内容型插件 —— 插件包从哪来、装给谁,是你自己的事。 下面所有字段与函数都以三个真实插件样例和 CJRT 的 ABI 文档核对过。

目录

  • CAT. 921 插件与来源 一个插件是什么,它和「来源」为什么不是一回事
  • CAT. 922 manifest.json 顶层字段与 music 扩展段
  • CAT. 923 kind 与能力集 library / scraper / provider,以及 capabilities
  • CAT. 924 要导出哪些函数 ABI 的函数面,按能力对应
  • CAT. 925 错误模型 musicError 与错误码
  • CAT. 926 凭据与存储 localStorage、键前缀,以及一个必须知道的坑
  • CAT. 927 版本与热更新 改了代码不改 version,等于没发
  • CAT. 928 打包与导入 目录结构、zip、URL
CAT. 921

插件与来源

一个插件是 plugin.jsmanifest.json 的分发单位,安装到 <AppSupport>/cjrt_plugins/<id>/。它是代码。

「来源」是用户在界面上添加、登录、启停、排序、删除的那个实体。它是实例。 来源 id 就是已安装的插件 id —— 同一份 plugin.js 以不同 id 安装两次, 就是两个凭据完全独立的来源,宿主侧一行代码都不用改。

标记为音乐库的插件可以这样派生多个来源;其余插件是一个插件对应一个来源。

↑ 回目录

CAT. 922

manifest.json

顶层描述插件包本身,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"]
  }
}

↑ 回目录

CAT. 923

kind 与能力集

kind 决定这个插件在产品里的身份,缺省是 provider

capabilities

界面按能力过滤:没声明 search 的插件不会出现在搜索的来源切换里,没声明 lyrics 的会被跳过,直接走应用层的多来源歌词查找。

↑ 回目录

CAT. 924

要导出哪些函数

按你声明的能力导出对应函数即可,没声明的不必实现。下表是三个真实插件里实际出现的导出面。 函数签名以 CJRT 的 ABI 文档为准,本表给出的是用途。

↑ 回目录

CAT. 925

错误模型

出错时抛一个 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(内容不存在或为空)。单个插件抛错不会拖垮整个应用, 只是该来源在这次调用里缺席。

↑ 回目录

CAT. 926

凭据与存储

插件自管凭据,用标准的 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));

↑ 回目录

CAT. 927

版本与热更新

最常见的坑

改了 plugin.js,必须把 manifest 的 version 往上抬。 宿主是按版本号比对来决定要不要覆写已装文件的。版本没变,已装用户就永远拿不到你的新代码 —— 你本地测得好好的,用户那边毫无变化。

版本变化时,宿主覆写 manifest.jsonplugin.js, 但保留 storage.json,所以用户的登录状态不受影响。

↑ 回目录

CAT. 928

打包与导入

一个插件包就是一个目录,里面三个文件:

<你的插件 id>/
  manifest.json
  plugin.js
  logo.png

用户在设置的来源管理里通过本地目录或 zip,或者一个 URL 导入。 导入时会校验 manifest.id 与安装用的 id 是否一致。

导入插件意味着在你的设备上运行第三方代码,Haiku 会就此提示用户。 这是插件机制的固有前提,写清楚你的插件做什么、连哪里,对用户和你都好。

↑ 回目录