Skip to content

插件协议版本、迁移与排障

本页用于回答三个开发问题:插件应该声明哪个版本、每个版本具体改了什么,以及插件在 Devkit 或真机中失败时从哪里开始排查。

先区分两个版本字段

manifest.json 中的两个版本字段控制不同的兼容边界,不能互相替代:

Manifest 字段当前宿主版本控制内容
apiVersion5插件回调 searchSongsgetLyricssearchCovers 的返回协议
minHostApiVersion4插件调用的 Platform.* 宿主函数集合

例如,一个返回 API5 扩展歌词候选、但只使用 Platform.http 的插件应声明:

json
{
  "apiVersion": 5,
  "minHostApiVersion": 1
}

宿主目前接受 apiVersion: 15,以及 minHostApiVersion: 14。声明更高版本时会在安装阶段被拒绝,避免插件进入运行阶段后才因未知协议或缺少宿主函数失败。

插件可以在运行时检查实际能力:

javascript
const runtime = Platform.runtime.getInfo();

Platform.log.debug("plugin", JSON.stringify({
  pluginApiVersion: runtime.pluginApiVersion,
  hostApiVersion: runtime.hostApiVersion,
  engine: runtime.engine,
  supportedHostApis: runtime.supportedHostApis
}));

if (!runtime.supportedHostApis.includes("cache.get")) {
  throw new Error("This plugin requires Platform.cache (Host API 3)");
}

pluginApiVersion 是宿主能够解析的最高插件协议版本,不是当前插件自己的 manifest.apiVersion

API1:基础插件回调协议

API1 定义了三个相互独立的全局回调。插件只需实现 capabilities 中声明的回调;旧插件未声明 capabilities 时按仅支持 searchSongs 处理。

回调请求API1 返回值用途
searchSongs(request){ keyword, page, pageSize, separator, config }SongSearchResult[]搜索歌曲和元数据
getLyrics(request){ song, config }单个 LyricsResult、LRC 字符串或 null获取一份歌词
searchCovers(request){ keyword, song?, pageSize, config }SongSearchResult[]搜索封面

API1 对应的基础 Platform 能力包括应用与运行时信息、HTTP、日志、常用加密、Base64、字节运算、解压和 XML 处理。完整签名见宿主 API

API2:新增 Base64URL 宿主函数

API2 没有改变三个插件回调的请求或返回值。变化发生在 Platform Host API 2,精确新增以下函数:

新增函数参数返回值
Platform.base64.encodeUrlText(text)UTF-8 文本无 padding 的 Base64URL 字符串
Platform.base64.decodeUrlText(base64Url)Base64URL 字符串UTF-8 文本
Platform.base64.encodeUrlBytes(bytes)字节数组无 padding 的 Base64URL 字符串
Platform.base64.decodeUrlBytes(base64Url)Base64URL 字符串字节数组
Platform.base64.toUrl(base64)标准 Base64 字符串Base64URL 字符串
Platform.base64.fromUrl(base64Url)Base64URL 字符串补齐 padding 的标准 Base64 字符串

只有实际调用这些函数的插件才需要 minHostApiVersion: 2。仅使用 API2 回调协议、但不依赖新增 Platform 函数的插件仍可声明 minHostApiVersion: 1

API3:新增插件私有缓存

API3 没有改变三个插件回调的请求或返回值。Platform Host API 3 精确新增四个缓存函数:

新增函数签名返回值与行为
读取Platform.cache.get(key)返回字符串;不存在、已过期或损坏时返回 ""
写入Platform.cache.set(key, value, ttlMs?)返回 ""ttlMs > 0 时到期,省略或传 0 时不过期
删除Platform.cache.remove(key)删除一个键并返回 ""
清空Platform.cache.clear()清空当前插件的所有缓存并返回 ""

缓存按插件 ID 隔离。它适合保存 cookie、匿名登录态和临时 token,不是歌曲搜索结果缓存;搜索页面的结果缓存由宿主界面管理,插件没有可调用的“搜索结果缓存函数”。

缓存对象或数组时,插件需要自行编码和解析,因为缓存只保存字符串:

javascript
const CACHE_KEY = "session.cookies";

function saveCookies(cookies) {
  Platform.cache.set(CACHE_KEY, JSON.stringify(cookies), 12 * 60 * 60 * 1000);
}

function loadCookies() {
  const raw = Platform.cache.get(CACHE_KEY);
  return raw ? JSON.parse(raw) : [];
}

这里的 JSON.stringify 用于把对象保存为缓存字符串,是正确用法;不要用它序列化插件回调的最终返回值。

API4:歌词与封面候选契约

API4 调整 getLyrics 和 searchCovers 的结果契约,使独立歌词源和封面源能够返回可识别的候选。TTML 载荷扩展属于 API5;国际化属于 Host API4。

三个回调的 API3 与 API4 对比

回调请求是否变化API1–3 返回值API4 返回值
searchSongsSongSearchResult[]不变
getLyrics是;新增可选 pagepageSize单个 LyricsResult、LRC 字符串或 nullLyricsResult[];每项以 tags.ti/ar/al/date 提供标题、艺术家、专辑、日期
searchCovers是;新增可选 pageSongSearchResult[],旧字段继续兼容SongSearchResult[];每项必须有标题、艺术家、专辑、日期和封面 URL,平台歌曲 id 可省略

当前宿主还会向 getLyrics 提供可选的 pagepageSize,供不实现 searchSongs 的 API4 歌词源分页返回候选。旧插件可以忽略这些新增字段;函数调用签名仍是单个 request 对象。

插件回调接收的是 JavaScript 对象,必须直接返回 JavaScript 值。Android 宿主会统一完成一次 JSON 序列化:

javascript
function getLyrics(request) {
  return [{
    type: "rawPlainLrc",
    tags: {
      ti: "歌曲标题",
      ar: "艺术家",
      al: "专辑",
      date: "2026"
    },
    rawPlainLrc: "[00:00.00]Example"
  }];
}

以下写法会被宿主再次序列化,形成字符串包字符串,解析必然失败:

javascript
// 错误:不要序列化回调的最终返回值
return JSON.stringify([{ id: "1", title: "Song" }]);

歌词结果的四项判断信息复用标准歌词标签,不增加重复的顶层字段:

歌词标签搜索结果中显示为
tags.ti标题
tags.ar艺术家
tags.al专辑
tags.date日期或年份

API4 封面结果示例:

javascript
function searchCovers(request) {
  return [{
    title: "歌曲标题",
    artist: "艺术家",
    album: "专辑",
    date: "2026",
    picUrl: "https://example.com/cover.jpg"
  }];
}

封面 URL 也兼容 coverUrlcover_urlartworkUrl。字段别名与完整歌词格式见插件函数

宿主的实际调用链

capabilities 决定插件在哪类调用中可用。元数据源同时用于单曲主搜索和批量元数据匹配;歌词源和封面源分别用于对应的单曲与批量操作。

场景会调用哪些源调用顺序
单曲主搜索已启用且具备 searchSongs 的元数据源先调用 searchSongs;只有结果所属插件同时声明 getLyrics 时,才显示歌词入口并调用 getLyrics
批量元数据匹配已启用且具备 searchSongs 的元数据源搜索、评分并写入非歌词、非封面字段
批量歌词匹配已启用且具备 getLyrics 的歌词源searchSongs 时先选歌,否则直接请求歌词候选
批量封面匹配已启用且具备 searchCovers 的封面源调用 searchCovers 并按本地歌曲信息评分
独立歌词搜索,有 searchSongs具备 getLyrics 且当前歌词页已启用的源先调用同一插件的 searchSongs;用户选择歌曲后,再把该结果原样传给同一插件的 getLyrics
独立歌词搜索,无 searchSongs具备 getLyrics 且当前歌词页已启用的 API4 源直接以当前本地歌曲信息调用 getLyrics
封面搜索具备 searchCovers 且当前封面页已启用的源直接调用各自的 searchCovers

歌曲 ID、internal、歌词和封面不会跨插件拼接。歌词页面中的“全部”只是在同一个页面保留并展示各源缓存的搜索结果,不会把一个源返回的歌曲交给另一个源获取歌词。

API5:歌词返回格式扩展与 TTML 信息保留

API5 在 API4 歌词候选格式上扩展逐词音译、行级属性、演唱者、head 元数据、时间粒度、语言码、段落时间窗、正文时长和带时间的多音节 Ruby 注音。候选数组、必填歌曲标签以及 searchSongs / searchCovers 的契约不变。这些扩展来自提交 0876c8151071e09e

继续兼容 API1–4 插件。扩展字段可选;声明 API5 不会自动补出源数据中缺失的信息。

结构化歌词的 TTML 元数据

type: "structured" 的歌词结果可以选择携带 TTML 专属信息。TTML 专属结构在导出为 LRC 时不会保留;逐词音译可在逐字 LRC 中保留时间戳。旧插件只返回 originaltranslatedromanization 时,与 API3 的行为一致。

载荷位置内容写回 TTML
original 行的第 4 个元素行级扩展属性,例如 ttm:agentitunes:song-partdivBegin/divEnd<p ttm:agent><div itunes:song-part>、段落 <div begin/end>
original 词的第 4 个元素Ruby 注音音节数组 [[startMs, endMs, "注音"], ...]<span tts:ruby="container"> / <span tts:ruby="text">,缺失的音节边界由宿主规范化
romanization 行第 3 个元素词数组或旧版整行文本head <transliteration> 中的定时 span;逐字 LRC 也支持音译时间戳
agents{ id, type?, name? }[]<head> 中的 <ttm:agent>
metadata{ name, namespace?, attributes?, text?, children? }[]<head> 中的元数据节点
timing"Word""Line"<tt itunes:timing>
language原文语言码(BCP 47)<tt xml:lang>
bodyDurTTML 时间表达式<body dur>
translatedLang / romanizationLang翻译轨 / 音译轨语言码(BCP 47)对应轨的 xml:lang

itunes:key 由宿主重新生成,插件不需要提供。扩展属性只接受无前缀名称与 ttm:itunes: 前缀,其他前缀会被忽略;非法的 bodyDur 会被丢弃。

字段格式、metadata 的约束、逐词时间与缺失边界的处理,以及 rawTtml 的应用场景见插件函数的「结构化歌词的行格式」与「TTML 扩展」。

Host API4:插件国际化

宿主 API4 独立新增两个本地化函数。仅使用 API5 歌词扩展并不要求 Host API4。

新增函数签名返回值与行为
当前语言Platform.i18n.getLocale()返回选中语言的标签,例如 "zh-Hans";插件没有 i18n 时返回 "und"
取文本Platform.i18n.t(key, ...args)返回该键在当前语言下的文本;传入参数时按位置占位符格式化

Manifest 中使用 @ 字符串引用,或脚本调用 Platform.i18n 的插件,需要把 minHostApiVersion 提高到 4。资源文件写法与占位符规则见插件国际化

从 API3 升级到 API4

  1. manifest.jsonapiVersion 改为 4
  2. 按实际实现填写 capabilities;不要声明未实现的回调。
  3. 保持 searchSongs 的请求和返回值不变。
  4. getLyrics 的单个结果改为数组;无结果返回 [],每项补齐 tags.titags.artags.altags.date
  5. 为每个 searchCovers 结果补齐 titleartistalbumdate 和封面 URL;id 可以省略。
  6. 所有回调直接返回对象、数组、字符串或 null,不要对最终返回值调用 JSON.stringify
  7. 只有使用了 Base64URL、缓存或文本本地化时,才把 minHostApiVersion 分别提高到 2、3 或 4。

从 API4 升级到 API5

  1. 将 manifest 中的 apiVersion 设为 5,保留 getLyrics 候选数组和 tags.ti/ar/al/date
  2. 有逐词音译时返回词数组;按源数据补充行级扩展、agents、metadata、语言与时间粒度、bodyDur 和 Ruby。旧的整行字符串仍有效。
  3. 遵循插件函数中的字段格式和校验规则;Ruby 缺失边界用 null,非法 bodyDur 会被丢弃。
  4. minHostApiVersion 按实际使用的 Platform 能力填写(1–4),不要改成 5;国际化需要 Host API4。
  5. 协议上限为 4 的旧宿主会拒绝 API5 插件,安装或验证前需更新 Lyrico 和 Devkit。

用 Devkit 定位问题

Lyrico-Plugins 仓库根目录执行:

bash
node tools/plugin-devkit/src/cli.js validate ./my-plugin
node tools/plugin-devkit/src/cli.js inspect ./my-plugin
node tools/plugin-devkit/src/cli.js test ./my-plugin searchSongs --keyword "晴天" --page-size 5
node tools/plugin-devkit/src/cli.js test ./my-plugin getLyrics --song ./song.json --logs
node tools/plugin-devkit/src/cli.js test ./my-plugin searchCovers --keyword "晴天" --page-size 5

增加 --json 可以输出完整的 request、宿主序列化后的 raw、解析后的 parsedwarningserrors 和插件日志。排查时先看第一条 errors,再对照 rawparsed

现象或错误先检查什么常见原因
安装时提示插件协议不支持manifest.apiVersion高于宿主支持的 5,或把 Platform 版本误填到了这里
安装时提示宿主 API 不支持manifest.minHostApiVersion高于宿主支持的 4
returned JSON.stringify(...) instead of a JavaScript value回调中的最终 return插件提前序列化,Android 宿主又序列化一次
getLyrics returned no usable lyrics candidatesraw、歌词 type 与对应载荷字段返回的数组为空,或歌词对象不能被解析
lyrics candidate[n] is missing ...tags.ti/ar/al/dateAPI4 歌词候选缺少用户判断信息
cover result[n] is missing ...title/artist/album/date 和封面 URLAPI4 封面候选字段不完整
单曲搜索中看不到插件searchSongs 能力与元数据源启用状态未声明 searchSongs,或未启用为元数据源
歌词搜索能搜到歌曲,点选后失败getLyrics --songraw/errors/logssearchSongs 正常不代表解密、歌词接口或结果格式正常
InternalError: interrupted单次回调耗时和循环规模Android QuickJS 的一次加载或调用有 15 秒执行期限;避免在一次 getLyrics 中批量请求并解密多首候选
Platform.xxx is not a functionPlatform.runtime.getInfo().supportedHostApisDevkit/宿主过旧,或 minHostApiVersion 声明过低
Devkit 通过但真机失败raw、Android 日志、QuickJS 兼容性网络/TLS、执行期限或 JavaScript 引擎差异;不要只验证 searchSongs

Devkit 用 Node.js 模拟宿主,不能替代 Android QuickJS 真机验证,但它应当先消除 manifest、返回协议、双重序列化和必填字段错误。