// Debug & Publish

调试与发布

在应用里看日志、用 Node.js 在应用外跑通插件、对照错误速查表排错,然后按约定把它分享出去。

插件日志

插件卡片上的「插件日志」显示这个插件最近的 500 行日志(每行最多 2000 个字符)。里面有两类内容:

最有用的一行 「返回值无效」之后跟着的是 Rust 反序列化器指出的具体问题:缺失的字段名、类型不匹配的位置。大多数「插件返回的数据格式无效」都能凭它一眼定位,对照 数据结构 修正即可。

在应用外调试

每次改动都重装再点界面会很慢。因为插件是普通的 CommonJS,你可以在 Node.js 里提供一个模拟的 cerumusic,直接调用方法并打印结果。下面的脚本(Node 18+)把 cerumusic.request 接到 fetch,storage 接到内存 Map:

// harness.js — run a list plugin outside the app: node harness.js plugin.list.js search '{"keyword":"hello","page":1,"limit":10}'
const fs = require('fs');
const path = require('path');
const [file, method, argJson = '{}', configJson = '{}'] = process.argv.slice(2);

const store = new Map();
globalThis.cerumusic = {
  config: JSON.parse(configJson),
  storage: {
    get: (k) => store.get(k),
    set: (k, v) => store.set(k, JSON.parse(JSON.stringify(v ?? null))),
    remove: (k) => store.delete(k),
  },
  async request(url, opts = {}) {
    const res = await fetch(url, {
      method: opts.method || 'GET',
      headers: opts.headers,
      body: opts.body && typeof opts.body !== 'string' ? JSON.stringify(opts.body) : opts.body,
    });
    const text = await res.text();
    let body = text;
    try { body = JSON.parse(text); } catch (_) {}
    return { statusCode: res.status, headers: Object.fromEntries(res.headers), body };
  },
  utils: { crypto: { md5: (s) => require('crypto').createHash('md5').update(s).digest('hex') } },
};

const plugin = require(path.resolve(file)).listPlugin;
const source = Object.keys(plugin.sources)[0];
plugin[method](source, JSON.parse(argJson))
  .then((r) => console.log(JSON.stringify(r, null, 2)))
  .catch((e) => { console.error('ERROR:', e.message); process.exit(1); });

用法:

# 搜索
node harness.js hello.list.js search '{"keyword":"hello","page":1,"limit":10}'

# 带配置调用 musicUrl(第 4 个参数是 cerumusic.config)
node harness.js hello.list.js musicUrl '{"song":{"songmid":"1"},"quality":"128k"}' '{"audioUrl":"https://example.com/a.mp3"}'
Node 比宿主宽松 Node 里能跑通不代表在宿主里一定能跑通:Node 没有 24 次请求、15 秒超时与 16 MiB 响应的限制,不会检查返回值的字段类型,也有 require、process。把它当作逻辑自检,最终仍要在应用里验证。同时要注意,宿主会重新执行整个脚本来处理每次调用,harness 不会模拟这一点,所以把状态写在模块顶层的变量里在 Node 里能跑、在应用里却不行。

错误速查表

你看到的信息可能的原因与处理
插件文件为空或超过 1MiB文件为空,或压缩前体积超过 1 MiB。去掉内嵌的大块数据。
插件初始化失败或不兼容当前宿主脚本有语法错误,或使用了宿主没有的语法 / 全局对象:import / export、顶层 await、require 在加载时就会失败。改用 module.exports,把异步初始化放进 ready Promise 或方法里。
缺少 listPlugin / Not a list or legacy service pluginmodule.exports.listPlugin 不存在或不是对象。检查导出的写法。
列表插件缺少名称listPlugin.name 为空。
列表插件未声明来源 / 没有声明有效来源及能力sources 缺失;来源 ID 不合法(须 2–16 位小写字母、数字、下划线,且以字母开头);capabilities 为空,或全是拼错的能力名。
插件缺少 xxx声明了能力,却没有实现对应方法,或方法被写在了 listPlugin 之外。
插件未声明该能力界面调用了你没有声明的能力;在 capabilities 里补上,或在界面里避免该入口。
插件返回的数据格式无效返回值与数据结构不符。看日志里的 [error] … 返回值无效 那一行,常见原因:必填字段缺失、字符串字段给了数字、NaN、发现页用了 camelCase。
插件请求已取消或次数超限单次调用的请求超过 24 次(音源插件 16 次)。合并请求,或用 requestAll 并减少每页展开的数量。
插件网络请求失败或超时目标服务器不可达、证书无效或响应太慢。单个请求最多等 15 秒。
地址只支持无用户信息的 HTTP/HTTPSURL 里带了 user:pass@,或协议不是 http / https。把账号放进请求头。
插件响应过大单个响应超过 16 MiB(音源插件 4 MiB)。换分页接口。
插件执行超时或取消整次调用超过 30 秒。减少串行请求。
插件没有返回播放地址musicUrl 返回的既不是字符串,也不是带 url 字段的对象。
来源在菜单里看不到来源只有在「由本插件提供播放」打开(或当前音源插件也支持该来源 ID)时才会出现;同时确认插件处于启用状态。
改了脚本但行为没变需要重新安装同名脚本才会生效。配置会保留。

发布前检查清单

版本与更新

提交到内置插件仓库

应用内置的列表插件放在独立仓库 mio-music-plugins,在主仓库里以子模块挂载到 plugins/,通过 include_str! 嵌入应用。向它提交时请遵守:

要随应用内置 内置插件由主仓库的 crates/mio-core/src/plugin_catalog.rs 登记,应用启动时自动安装并按 @version 更新,还需要更新子模块指针。这一步会改动应用本身,请先在主仓库开一个 Issue 说明你的插件,再一起推进。

不随应用内置也完全可以:把 .js 文件放在任何地方,用户通过「安装列表插件」选择文件或输入 URL 就能安装。

Mio Music 只提供插件运行框架,不托管、不获取、不传输任何音乐数据。你的插件所访问的服务与内容是否合法、是否有权使用,由你与使用者自行负责。请不要发布以绕过付费、版权或访问控制为目的的插件。