// Debug & Publish
调试与发布
在应用里看日志、用 Node.js 在应用外跑通插件、对照错误速查表排错,然后按约定把它分享出去。
插件日志
插件卡片上的「插件日志」显示这个插件最近的 500 行日志(每行最多 2000 个字符)。里面有两类内容:
- 你的
console.log / info / debug / warn / error输出,前缀为[log]、[info]、[warn]等;多个参数会用空格拼接,对象会被JSON.stringify。 - 宿主的记录,前缀为
[info]或[error],例如[info] 列表插件已加载:…,来源 hello、[error] search 失败:…、[error] search 返回值无效:missing field `allPage`。
最有用的一行
「返回值无效」之后跟着的是 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 plugin | module.exports.listPlugin 不存在或不是对象。检查导出的写法。 |
| 列表插件缺少名称 | listPlugin.name 为空。 |
| 列表插件未声明来源 / 没有声明有效来源及能力 | sources 缺失;来源 ID 不合法(须 2–16 位小写字母、数字、下划线,且以字母开头);capabilities 为空,或全是拼错的能力名。 |
| 插件缺少 xxx | 声明了能力,却没有实现对应方法,或方法被写在了 listPlugin 之外。 |
| 插件未声明该能力 | 界面调用了你没有声明的能力;在 capabilities 里补上,或在界面里避免该入口。 |
| 插件返回的数据格式无效 | 返回值与数据结构不符。看日志里的 [error] … 返回值无效 那一行,常见原因:必填字段缺失、字符串字段给了数字、NaN、发现页用了 camelCase。 |
| 插件请求已取消或次数超限 | 单次调用的请求超过 24 次(音源插件 16 次)。合并请求,或用 requestAll 并减少每页展开的数量。 |
| 插件网络请求失败或超时 | 目标服务器不可达、证书无效或响应太慢。单个请求最多等 15 秒。 |
| 地址只支持无用户信息的 HTTP/HTTPS | URL 里带了 user:pass@,或协议不是 http / https。把账号放进请求头。 |
| 插件响应过大 | 单个响应超过 16 MiB(音源插件 4 MiB)。换分页接口。 |
| 插件执行超时或取消 | 整次调用超过 30 秒。减少串行请求。 |
| 插件没有返回播放地址 | musicUrl 返回的既不是字符串,也不是带 url 字段的对象。 |
| 来源在菜单里看不到 | 来源只有在「由本插件提供播放」打开(或当前音源插件也支持该来源 ID)时才会出现;同时确认插件处于启用状态。 |
| 改了脚本但行为没变 | 需要重新安装同名脚本才会生效。配置会保留。 |
发布前检查清单
- 脚本头部注释有
@name、@version、@author、@description,且与对象字段一致。 - 每个声明的能力都有对应方法;每个方法都能处理「用户没填配置」「服务器返回错误」「列表为空」三种情况。
- 所有返回值都对照 数据结构 检查过,数字字段是有限整数,字符串字段是字符串。
- 没有在模块顶层发请求;登录会话放在
cerumusic.storage,并随配置变化失效。 - 错误信息对用户友好,且不含密码、令牌或带签名的 URL。
- 对
http://服务器,参考内置插件的做法:要求用户显式勾选「允许 HTTP(不加密)」(回环地址除外),不要静默降级到明文传输。 - 在真实服务器上试过搜索、播放、歌词与(如有)导入歌单;没有条件验证的能力,在说明里如实标注。
- 脚本里没有任何属于你自己的密钥或凭据。
版本与更新
- 每次修改脚本都递增
version(注释里的@version与对象里的version同步)。目录靠它判断「可更新」。 - 用户重装同名脚本即为更新,配置保留。因此配置项的
key不要随意改名,需要改时在代码里兼容旧 key。 - 来源 ID 一经发布不要改变:用户收藏、歌单与下载记录里的歌曲通过它找回你的插件。
提交到内置插件仓库
应用内置的列表插件放在独立仓库 mio-music-plugins,在主仓库里以子模块挂载到 plugins/,通过 include_str! 嵌入应用。向它提交时请遵守:
- 脚本放在仓库根目录,命名为
<名称>.list.js。因为嵌入路径是固定的,文件名提交后不要再改。 - 脚本是自包含的单文件,不依赖任何打包或外部资源。
- 在仓库的 README 表格里补一行:文件、来源 ID、能力;并在「验证状态」里如实写明对哪些真实服务器验证过、哪些只在模拟服务上跑通。
- 修改已有脚本时同步递增版本号。
要随应用内置
内置插件由主仓库的
crates/mio-core/src/plugin_catalog.rs 登记,应用启动时自动安装并按 @version 更新,还需要更新子模块指针。这一步会改动应用本身,请先在主仓库开一个 Issue 说明你的插件,再一起推进。
不随应用内置也完全可以:把 .js 文件放在任何地方,用户通过「安装列表插件」选择文件或输入 URL 就能安装。
合规
Mio Music 只提供插件运行框架,不托管、不获取、不传输任何音乐数据。你的插件所访问的服务与内容是否合法、是否有权使用,由你与使用者自行负责。请不要发布以绕过付费、版权或访问控制为目的的插件。