Nuclear 插件开发指南:从目录结构、Manifest 到 Provider 注册与插件商店发布
【免费下载链接】nuclearStreaming music player that finds free music for you项目地址: https://gitcode.com/GitHub_Trending/nu/nuclear
本篇技术文章基于 Nuclear 仓库内的插件编写技能文档 .agents/skills/writing-plugins/SKILL.md,结合plugin-sdk包与播放器端插件加载器(PluginLoader、esbuild-wasm 编译器)的真实源码,完整讲解 Nuclear 插件的运行模型、目录结构、package.jsonManifest 规范、Streaming/Metadata 两种 Provider 的注册方式、全部可用 API,以及打包发布到插件商店的完整流程。读完后你可以独立搭建一个可被 Nuclear 在浏览器内编译加载的插件,并完成从 GitHub Release 到plugin-registry提交的发布闭环。
一、执行模型:插件是独立仓库,在浏览器内经 esbuild-wasm 编译
Nuclear 插件是独立的仓库(standalone repos),不内置于主程序,且在浏览器(Tauri WebView)内通过 esbuild-wasm 编译。理解这一模型是理解后续所有结构约定的前提。编译器实现在 pluginCompiler.ts 中,源码注释与实现透露了几个关键设计决策:
- 为什么用 esbuild-wasm:Tauri WebView 里没有 Node 的
fs,也没有原生 esbuild 二进制,因此插件即使写成 TypeScript,也能在浏览器上下文中被编译后执行(见 pluginCompiler.ts 顶部注释 L1-L21)。 - 虚拟文件系统:编译器通过自定义的
tauri-fsesbuild 插件,用 Tauri 的readTextFile读取插件目录下的相对导入,全程不接触 Node fs(pluginCompiler.ts)。 - 纯 JS 文件跳过编译:
compilePlugin只对.ts/.tsx入口做编译,.js入口直接读取文本执行(pluginCompiler.ts)。 - SDK 被标记为 external:
@nuclearplayer/plugin-sdk不在插件 bundle 中打包,而是运行时由宿主注入,避免插件意外捆绑宿主依赖(pluginCompiler.ts)。 - 输出为 CJS:编译产物
format: 'cjs'、jsx: 'automatic'、内联 sourcemap,目标 ES2022,因此插件默认导出必须兼容module.exports.default的 CommonJS 环境。 - 编译缓存:以入口路径为 key,记录参与上一次构建的所有文件的内容哈希;任何一个被导入文件发生变化都会触发重新编译,保证编辑后 reload 拿到的是新代码(pluginCompiler.ts)。
加载与沙箱化逻辑集中在 PluginLoader.ts:插件代码最终通过new Function('exports', 'module', 'require', code)在受控环境中求值,require是一个白名单 shim,只允许四个模块——@nuclearplayer/plugin-sdk、@nuclearplayer/ui、react、react/jsx-runtime,任何其它模块名都会抛出Module not found(PluginLoader.ts)。插件必须导出一个默认对象,否则加载失败。
二、插件目录结构与入口点
按 SKILL.md 的最小结构,一个插件仓库长这样:
my-plugin/ package.json # 带 nuclear 元数据的 Manifest src/ index.ts # 入口点,默认导出 NuclearPlugin如果插件使用本地构建工具(如 tsup)预先打包,结构可以扩展为带dist/的形态(参见 plugin-sdk README)。
生命周期钩子
入口文件默认导出一个NuclearPlugin对象。SDK 的类型定义在 types.ts:
export type NuclearPlugin = { onLoad?(api: NuclearPluginAPI): void | Promise<void>; onUnload?(api: NuclearPluginAPI): void | Promise<void>; onEnable?(api: NuclearPluginAPI): void | Promise<void>; onDisable?(api: NuclearPluginAPI): void | Promise<void>; };四个钩子全部可选,按 PluginLoader.ts 的逻辑,load()在解析完代码后若检测到onLoad会立即调用(并await)。SKILL.md 给出的标准骨架:
import type { NuclearPlugin, NuclearPluginAPI } from '@nuclearplayer/plugin-sdk'; const plugin: NuclearPlugin = { onLoad(api: NuclearPluginAPI) {}, onEnable(api: NuclearPluginAPI) { // 在这里注册 Provider }, onDisable() { // 在这里注销 Provider }, onUnload() {}, }; export default plugin;各钩子语义(plugin-sdk README):
onLoad(api)— 插件代码加载完成、Manifest 解析后执行;onEnable(api)— 用户在设置中启用插件时执行,注册 Provider 的正确时机;onDisable(api)— 用户禁用插件时执行,注销 Provider 的正确时机;onUnload(api)— 插件从内存移除前执行。
入口文件的解析顺序
Manifest 未声明main时,PluginLoader.ts 按以下候选顺序探测第一个存在的文件:index.js、index.ts、index.tsx、dist/index.js、dist/index.ts、dist/index.tsx;全部找不到则抛出明确的错误信息。注意 Manifest 层面对缺失main只产生警告("will attempt fallback resolution"),不会阻断加载(pluginManifest.ts)。
三、Manifest:package.json 的字段规范
SKILL.md 中的完整 Manifest 示例:
{ "name": "nuclear-plugin-example", "version": "0.1.0", "description": "What this plugin does", "author": "Your Name", "license": "AGPL-3.0-only", "main": "src/index.ts", "type": "module", "nuclear": { "displayName": "Example Plugin", "category": "streaming", "icon": { "type": "link", "link": "https://example.com/icon.svg" } }, "dependencies": { "@nuclearplayer/plugin-sdk": "^1.1.0" } }字段校验:zod Schema 的确切行为
宿主端使用 zod 对package.json做严格校验,实现在 pluginManifest.ts:
| 字段 | 是否必填 | 校验/归一化行为 |
|---|---|---|
name | 必填 | 非空字符串,加载时 trim;作为插件唯一 id |
version | 必填 | 非空字符串,Semver 约定,trim |
description | 必填 | 非空字符串,trim |
author | 必填 | 非空字符串,trim |
main | 可选 | 缺省时给出警告并走候选文件回退解析 |
nuclear | 可选 | 见下表 |
整个 Schema 使用.passthrough(),因此额外字段(如license、dependencies)不会导致校验失败。SDK 侧对应的类型定义在 types.ts:
nuclear子字段 | 说明 | 源码依据 |
|---|---|---|
displayName | 展示名,缺省回退到name | PluginLoader.ts |
category | 单分类(过渡字段) | pluginManifest.ts,源码注释标注待迁移到categories后移除 |
categories | 分类数组,缺省由category派生为单元素数组 | pluginManifest.ts |
icon | 仅支持{ "type": "link", "link": "url" },严格模式拒绝多余键 | pluginManifest.ts |
permissions | 信息性声明;解析时自动 trim、去重(去重会写入警告)、按字母序排序 | pluginManifest.ts |
另外,nuclear中出现displayName/category/categories/icon/permissions之外的未知键也会产生警告(pluginManifest.ts),是排查拼写错误的线索。
插件可用的categories取值:SKILL.md 面向 Provider 注册场景列出了streaming、metadata、lyrics三类;面向商店提交的完整合法值更多,packages/docs的 publishing.md 给出的注册表合法分类为:streaming、metadata、lyrics、scrobbling、dashboard、playlists、discovery、other,且注册表提交时必填。SDK 端的ProviderKind类型与此呼应,除了上述核心种类外还开放了任意字符串扩展(providers.ts)。
四、Provider 类型与注册:Streaming 和 Metadata
Streaming Provider:把曲目解析为可播放音频流
SDK 中流媒体 Provider 的完整类型定义在 types/streaming.ts:
export type StreamingProvider = ProviderDescriptor<'streaming'> & { searchForTrack: ( artist: string, title: string, album?: string, ) => Promise<StreamCandidate[]>; searchForTrackV2?: (track: Track) => Promise<StreamCandidate[]>; getStreamUrl: (candidateId: string) => Promise<Stream>; getStreamUrlV2?: (candidate: StreamCandidate) => Promise<Stream>; supportsLocalFiles?: boolean; };SKILL.md 的最小实现示例:
const provider: StreamingProvider = { id: 'my-streaming', kind: 'streaming', name: 'My Streaming', async searchForTrack(artist, title, album?) { /* return StreamCandidate[] */ }, async getStreamUrl(candidateId) { /* return Stream */ }, }; api.Providers.register(provider); api.Providers.unregister('my-streaming');两点源码层面的补充:
- 除了
searchForTrack,还实现了searchForTrackV2(接收完整Track对象)与getStreamUrlV2(接收完整StreamCandidate),宿主会优先使用 V2 签名以便传递更完整的上下文——从类型定义的结构可以推断 V2 是新增能力的向后兼容扩展。 id/kind/name来自通用描述符ProviderDescriptor(providers.ts),注册进宿主后由ProvidersHost统一管理:register返回 provider id,unregister返回是否成功,另外还提供list(kind?)、get、getActive、setActive、subscribe等方法(providers.ts)。api.Providers门面封装在 api/providers.ts,直接透传ProvidersHost。
Metadata Provider:搜索艺术家/专辑并拉取详情
SKILL.md 给出的元数据 Provider 形态:
const provider: MetadataProvider = { id: 'my-metadata', kind: 'metadata', name: 'My Metadata', searchCapabilities: ['artists', 'albums'], streamingProviderId: 'my-streaming', // 可选:将流媒体锁定到指定 Provider async searchArtists(params) { /* ... */ }, async searchAlbums(params) { /* ... */ }, async fetchArtistBio(id) { /* ... */ }, async fetchAlbumDetails(id) { /* ... */ }, };其中searchCapabilities声明该 Provider 支持的搜索维度,streamingProviderId是可选字段:声明后,使用该元数据源检索出的曲目会锁定由对应 streaming Provider 解析流地址,保证"能搜到就能播"。
宿主侧暴露给插件反查元数据的能力,由MetadataHost类型定义(types/metadata.ts):search、fetchArtistBio、fetchArtistSocialStats、fetchArtistAlbums、fetchArtistTopTracks、fetchArtistPlaylists、fetchArtistRelatedArtists、fetchAlbumDetails。params/Album/ArtistBio等模型类型统一来自@nuclearplayer/model包,并由 plugin-sdk 入口 再导出,插件直接import type { Album, Track, ArtistCredit } from '@nuclearplayer/plugin-sdk'即可使用。
五、可用 API 全览
SKILL.md 列出的核心 API:
api.Providers— 注册/注销 Providerapi.Settings— 插件设置存储api.Http— fetch 封装api.Ytdlp— yt-dlp 集成api.Queue— 播放队列控制api.Metadata— 搜索音乐元数据api.Streaming— 解析流地址api.Logger— 结构化日志(trace/debug/info/warn/error)
实际上 SDK 提供的NuclearPluginAPI(即NuclearAPI的子类)覆盖面更广,api/index.ts 中共暴露 15 个域 API:
| API | 用途 |
|---|---|
api.Settings | 定义、读取、持久化插件设置,并可注册自定义设置控件(widget) |
api.Providers | 注册/注销 Provider |
api.Queue | 读取与操作播放队列 |
api.Streaming | 通过候选曲目解析音频流 |
api.Metadata | 搜索/拉取艺术家、专辑、曲目详情 |
api.Http | 从插件发起 HTTP 请求并绕过 CORS |
api.Ytdlp | yt-dlp 搜索与流信息 |
api.Favorites | 管理用户收藏曲目 |
api.Logger | 结构化日志 |
api.Dashboard | 获取仪表盘内容(热门曲目、新发行等) |
api.Discovery | 从 Provider 获取曲目推荐 |
api.Playback | 控制播放、音量、随机、循环 |
api.Playlists | 创建、更新、删除播放列表 |
api.Events | 订阅播放器生命周期事件(如曲目播放结束) |
api.Shell | 在系统浏览器中打开 URL |
宿主为每个插件实例化这套 API 的接线代码在 createPluginAPI.ts:其中Settings使用按pluginId/displayName隔离的设置宿主(createPluginSettingsHost),Logger使用按插件 id 划分的日志宿主(createLoggerHost),其余为进程内共享的单例宿主。
以api.Settings为例,api/settings.ts 提供了register(SettingDefinition[])声明式定义设置项、get/set读写插件私有设置、getGlobal/setGlobal读写全局设置、subscribe监听变更,以及registerWidget/unregisterWidget注册自定义 React 设置控件(控件渲染依赖编译时注入的@nuclearplayer/ui与react白名单模块)。SDK 还导出useSettingReact hook(index.ts)供 TSX 插件消费。
六、本地开发与调试流程
SDK README 给出的快速起步:
mkdir my-plugin && cd my-plugin pnpm init -y pnpm add @nuclearplayer/plugin-sdk创建src/index.ts并默认导出生命周期对象后,可以按需选择两条路径:
- 不预编译:直接把
.ts源码打包进plugin.zip,Nuclear 会用 esbuild-wasm 即时编译; - 预编译(推荐,加载更快):用任意能输出单文件的 bundler。SDK README 给出的 tsup 示例:
{ "devDependencies": { "tsup": "^8" }, "scripts": { "build": "tsup src/index.ts --dts --format cjs --minify --out-dir dist" } }产物必须兼容 CommonJS 环境(module.exports或exports.default)。日常开发循环是:修改代码 → 重新构建 → 在 Nuclear 中重新加载插件。注意每次改动后都需要 reload 插件才能生效;编译缓存的失效逻辑(前文第二节)保证 reload 拿到的一定是重新编译后的代码。
七、发布:GitHub Release + 注册表提交
Nuclear 插件商店由一个静态注册表支撑:注册表只保存插件元数据(名称、仓库、分类),插件代码本体留在开发者自己的 GitHub 仓库中;用户安装时,Nuclear 从该仓库拉取最新 GitHub Release 的plugin.zip。发布共三步:
1. 打 tag 触发自动发版
按 SKILL.md,在插件仓库添加.github/workflows/release.yml:
name: Release on: push: tags: ['v*'] jobs: release: runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v4 - run: zip -r plugin.zip src package.json README.md - uses: softprops/action-gh-release@v2 with: files: plugin.zip generate_release_notes: true然后打 tag 推送:git tag v0.1.0 && git push origin v0.1.0。
2. plugin.zip 的内容约定
publishing.md 明确:Nuclear 只认最新 Release 中名为plugin.zip的资源,缺失即安装失败。zip 内文件必须位于根层级,不能套子目录:
plugin.zip ├── index.js # 入口(或 main 指向的文件) ├── package.json # 插件元数据 └── ... # 插件需要的其它文件如果 zip 里是my-plugin/index.js这种带子目录的结构,插件将无法加载。由于 Nuclear 支持即时编译 TS,zip 里可以直接放.ts/.tsx源码,但预编译产物加载更快。
3. 向 plugin-registry 提交 PR
ForkNuclearPlayer/plugin-registry,把插件条目加入plugins.json的plugins数组,并发起 PR。条目字段约束(publishing.md):
| 字段 | 必填 | 约束 |
|---|---|---|
id | 是 | 必须与package.json的name一致;小写、连字符、2-64 字符 |
name | 是 | 展示名,1-64 字符 |
description | 是 | 10-200 字符 |
author | 是 | 1-64 字符 |
repo | 是 | owner/repo-name格式 |
category | 是 | 必须与package.json的nuclear.category一致 |
categories | 是 | 与插件 Provider 类型匹配的分类数组 |
tags | 否 | 至多 10 个,小写连字符、去重 |
version | 否 | 由 CI 自动填充 |
downloadUrl | 否 | 最新plugin.zip直链,由 CI 自动填充 |
addedAt | 是 | ISO 8601 时间戳 |
示例条目:
{ "id": "nuclear-plugin-discogs", "name": "Discogs", "description": "Fetch album and artist metadata from Discogs", "author": "nukeop", "repo": "NuclearPlayer/nuclear-plugin-discogs", "category": "metadata", "categories": ["metadata"], "tags": ["discogs", "metadata"], "version": "1.0.0", "downloadUrl": "https://github.com/NuclearPlayer/nuclear-plugin-discogs/releases/download/v1.0.0/plugin.zip", "addedAt": "2026-01-25T00:00:00Z" }版本更新策略
发布新版本不需要改注册表:只需创建带新plugin.zip的 GitHub Release,用户下次安装或自动更新时即会拉到。Nuclear 在启动时检查插件更新,自动更新默认开启,用户可在设置的 Plugins 页关闭(见 publishing.md 及宿主侧自动更新实现 pluginAutoUpdate.ts)。只有变更描述、分类、标签等元数据时才需要再提注册表 PR。
参考路径汇总
- 技能文档:.agents/skills/writing-plugins/SKILL.md
- SDK 与入门:packages/plugin-sdk/README.md、packages/plugin-sdk/src/index.ts、packages/plugin-sdk/src/types.ts
- Provider/流媒体/元数据类型:packages/plugin-sdk/src/types/providers.ts、packages/plugin-sdk/src/types/streaming.ts、packages/plugin-sdk/src/types/metadata.ts
- 宿主加载链:PluginLoader.ts、pluginCompiler.ts、pluginManifest.ts、createPluginAPI.ts
- 发布文档:packages/docs/plugins/publishing.md,以及 providers.md、streaming.md、plugin-system.md 等专题文档可进一步深入
【免费下载链接】nuclearStreaming music player that finds free music for you项目地址: https://gitcode.com/GitHub_Trending/nu/nuclear
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考