Summary
从零开始开发 MusicFree 音乐播放器插件的完整指南。 当用户要求编写、创建、开发 MusicFree 插件,或要求将某个音乐网站、API 适配为 MusicFree 插件时触发。 涵盖:插件协议、媒体类型定义、方法签名、沙箱环境、站点分析、本地调试、发布更新等完整流程。 本 Skill 面向 AI 执行,指导…
maotoumao/musicfree-skills
从零开始开发 MusicFree 音乐播放器插件的完整指南。 当用户要求编写、创建、开发 MusicFree 插件,或要求将某个音乐网站、API 适? 涵盖:插件协议、媒体类型定义、方法签名、沙箱环境、站点分析、本地调试、发布更新等完整流程。 本 Skill 面向 AI 执行,指导 AI 与零基础用户协作完成插件开发。
npx skills add maotoumao/musicfree-skills --skill musicfree-plugin-dev
从零开始开发 MusicFree 音乐播放器插件的完整指南。 当用户要求编写、创建、开发 MusicFree 插件,或要求将某个音乐网站、API 适配为 MusicFree 插件时触发。 涵盖:插件协议、媒体类型定义、方法签名、沙箱环境、站点分析、本地调试、发布更新等完整流程。 本 Skill 面向 AI 执行,指导…
Other skills from maotoumao/musicfree-skills.
npx skills add maotoumao/musicfree-skills
Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.
main
Files included with this skill beyond the listing page.
SKILL.md
18,215 B
SUMMARY.md
452 B
以下规则在整个插件开发过程中始终生效,违反将导致开发失败:
/api/search、/v1/song 等路径。每个 URL 必须来自页面内容或 Playwright 捕获的网络请求当遇到困难时,唯一正确的做法是:用 Playwright 观察浏览器的真实行为。不要猜测,不要搜索,去观察。
能自己做的,绝不让用户做。 遵循以下优先级:
1. 收集信息 → 2. 判断路径 → 3. 分析数据源 → 4. 确定方法集合
→ 5. 逐个实现 → 6. 测试验证 → 7. 迭代修复 → 8. 输出最终插件
先检查开发环境(在做任何其他事之前):
node --versionnpm install -D playwright(不需要下载 Chromium,后续用 channel: 'chrome' 复用用户的 Chrome)然后收集需求:
author 字段)根据收集到的信息,选择对应路径:
路径 A:用户提供了 API 文档或接口规格 → 阅读文档 → 将端点映射到插件方法 → 直接编写代码
路径 B:用户提供了网站 URL(页面内容直接可抓取) → 用工具抓取页面 HTML → 分析 DOM 结构 → 用 cheerio 编写解析逻辑 → 详见 [references/site-analysis-playbook.md](references/site-analysis-playbook.md) "静态站点分析"
路径 C:用户提供了网站 URL(需逆向分析内部 API) 页面通过内部 API 异步加载数据,没有公开文档。需要观察浏览器的实际行为,然后用 axios 精确复现。
核心方法论——观察 → 分析 → 复现 → 验证:
→ 详见 [references/site-analysis-playbook.md](references/site-analysis-playbook.md)
快速预判站点类型:用工具抓取目标 URL 的 HTML,检查内容:
<script> 标签、或核心内容区域为空 → SPA 站点,直接使用 Playwright,不要在 axios 上浪费时间遵循"观察-分析-复现-验证"方法论(详见 [references/site-analysis-playbook.md](references/site-analysis-playbook.md))。
关键规则:
确定数据源后:
search 和 getMediaSource 开始(最核心的两个方法).js 文件// 引入需要的模块(均为沙箱内置,无需安装)
const axios = require('axios');
const cheerio = require('cheerio');
module.exports = {
// ===== 必填属性 =====
platform: '插件名称',
// ===== 可选属性 =====
version: '0.0.1', // 语义化版本号
author: '作者名',
description: '插件说明',
srcUrl: 'https://example.com/plugin.js', // 远程更新地址
cacheControl: 'no-cache', // "cache" | "no-cache" | "no-store"
supportedSearchType: ['music', 'album', 'artist', 'sheet'],
userVariables: [{ key: 'cookie', name: 'Cookie', hint: '请输入你的 Cookie' }],
hints: {
importMusicSheet: ['支持以下格式的链接:...', 'https://example.com/playlist/123'],
},
// ===== 方法 =====
async search(query, page, type) {
/* ... */
},
async getMediaSource(musicItem, quality) {
/* ... */
},
// ... 更多方法
};
| 属性 | 必填 | 说明 |
|---|---|---|
platform |
是 | 插件名称,不可为"本地" |
version |
否 | 语义化版本号,默认 "0.0.0" |
author |
否 | 插件作者 |
description |
否 | 说明文字 |
srcUrl |
否 | 远程 .js 文件直链,用于应用内更新 |
cacheControl |
否 | getMediaSource 结果的缓存策略 |
supportedSearchType |
否 | 声明支持的搜索类型数组,不填则认为全部支持 |
userVariables |
否 | 用户可配置变量数组,每项含 key(必填)、name、hint |
hints |
否 | 提示文案,键为方法名,值为字符串数组 |
userVariables 用法定义:
userVariables: [{ key: 'cookie', name: 'Cookie', hint: '在浏览器登录后获取' }];
运行时读取:
const cookie = env.getUserVariables().cookie;
所有方法均为 async,遇到错误应直接 throw。 完整签名、参数与返回值详见 [references/plugin-protocol.md](references/plugin-protocol.md)。 基本媒体类型字段详见 [references/media-types.md](references/media-types.md)。
| 方法 | 功能 | 核心入参 | 核心返回值 |
|---|---|---|---|
search |
搜索 | (query, page, type) |
{ isEnd, data: [] } |
getMediaSource |
获取播放链接 | (musicItem, quality) |
{ url, headers? } |
getLyric |
获取歌词 | (musicItem) |
{ rawLrc?, translation? } |
getAlbumInfo |
专辑详情 | (albumItem, page) |
{ isEnd, musicList, albumItem? } |
getMusicSheetInfo |
歌单详情 | (sheetItem, page) |
{ isEnd, musicList, sheetItem? } |
getArtistWorks |
歌手作品 | (artistItem, page, type) |
{ isEnd, data: [] } |
getMusicInfo |
补全歌曲信息 | (musicItem) |
Partial<IMusicItem> |
importMusicItem |
导入单曲 | (urlLike) |
IMusicItem |
importMusicSheet |
导入歌单 | (urlLike) |
IMusicItem[] |
getTopLists |
排行榜列表 | 无 | [{ title?, data: [] }] |
getTopListDetail |
排行榜详情 | (topListItem, page) |
{ isEnd, musicList } |
getRecommendSheetTags |
推荐歌单标签 | 无 | { pinned?, data: [] } |
getRecommendSheetsByTag |
按标签获取歌单 | (tag, page) |
{ isEnd, data: [] } |
getMusicComments |
歌曲评论 | (musicItem, page) |
{ isEnd, data: [] } |
page 从 1 开始;isEnd 为 true 表示到达最后一页,不填默认 trueplatform 自动注入:返回的媒体项无需设置 platform,框架会自动添加id 必须有:每个媒体项必须包含 id 字段IMusicItem 等类型支持任意可序列化的扩展字段,这些字段会在后续方法调用时被传入。利用这一特性在 search 中附带后续方法所需的额外数据throw,不要返回 null插件运行在隔离沙箱中(vm.createContext),有 10 秒执行超时。
require 引入)| 模块 | 用途 |
|---|---|
axios |
HTTP 请求 |
cheerio |
HTML 解析(类 jQuery 语法) |
crypto-js |
加解密 |
dayjs |
日期时间处理 |
big-integer |
大整数运算 |
qs |
URL 参数序列化 |
he |
HTML 实体编码/解码 |
webdav |
WebDAV 操作 |
注意:不在此列表中的模块无法 require。如需使用其它库(如 lodash),必须用 webpack 等工具将源码打包到插件文件中。
fetch, URL, URLSearchParams, AbortControllerBuffer, TextEncoder, TextDecoderbtoa, atob, encodeURIComponent, decodeURIComponentJSON, console.log/warn/errorsetTimeout, setInterval, Promiseenv.getUserVariables() — 读取用户变量env.os — 操作系统env.appVersion — 应用版本env.lang — 当前语言已知某服务的 API 接口,直接请求并转换数据格式:
const axios = require('axios');
module.exports = {
platform: '示例API源',
version: '0.0.1',
supportedSearchType: ['music'],
async search(query, page, type) {
if (type !== 'music') return { isEnd: true, data: [] };
const res = await axios.get('https://api.example.com/search', {
params: { keyword: query, page, limit: 20 },
});
const list = res.data.songs || [];
return {
isEnd: list.length < 20,
data: list.map((item) => ({
id: item.songId,
title: item.songName,
artist: item.singerName,
album: item.albumName,
artwork: item.coverUrl,
duration: item.duration,
// 扩展字段:后续 getMediaSource 会用到
extraId: item.fileHash,
})),
};
},
async getMediaSource(musicItem, quality) {
const res = await axios.get('https://api.example.com/play', {
params: { hash: musicItem.extraId, quality },
});
if (!res.data.url) throw new Error('无法获取播放链接');
return { url: res.data.url };
},
};
目标网站没有公开 API,直接抓取 HTML 并用 cheerio 解析:
const axios = require('axios');
const cheerio = require('cheerio');
module.exports = {
platform: '示例HTML源',
version: '0.0.1',
supportedSearchType: ['music'],
async search(query, page, type) {
if (type !== 'music') return { isEnd: true, data: [] };
const rawHtml = (
await axios.get('https://example.com/search', {
params: { q: query, page },
})
).data;
const $ = cheerio.load(rawHtml);
const results = [];
$('.search-result-item').each((i, el) => {
results.push({
id: $(el).attr('data-id'),
title: $(el).find('.song-title').text().trim(),
artist: $(el).find('.artist-name').text().trim(),
artwork: $(el).find('img').attr('src'),
url: $(el).find('audio').attr('src'), // 如果页面直接包含音源
});
});
return {
isEnd: $('.next-page').length === 0,
data: results,
};
},
};
音源 URL 可直接由已知信息拼接得出:
module.exports = {
platform: '示例拼接源',
version: '0.0.1',
cacheControl: 'no-store',
async getMediaSource(musicItem, quality) {
return {
url: 'https://cdn.example.com/audio/' + musicItem.id + '.mp3',
};
},
};
完成插件编写后,创建一个测试脚本在终端中运行:
// test-plugin.js
const plugin = require('./my-plugin.js');
async function test() {
console.log('=== 测试 search ===');
try {
const searchResult = await plugin.search('测试关键词', 1, 'music');
console.log('搜索结果数量:', searchResult.data.length);
console.log('是否最后一页:', searchResult.isEnd);
if (searchResult.data.length > 0) {
console.log('第一条结果:', JSON.stringify(searchResult.data[0], null, 2));
console.log('\n=== 测试 getMediaSource ===');
const source = await plugin.getMediaSource(searchResult.data[0], 'standard');
console.log('播放链接:', source?.url ? '✓ 获取成功' : '✗ 获取失败');
console.log('详细信息:', JSON.stringify(source, null, 2));
}
} catch (e) {
console.error('测试失败:', e.message);
}
}
test();
执行方式:
node test-plugin.js
测试要点:
id 字段存在且有效getMediaSource 返回的 URL 可访问注意:本地 Node.js 测试时,env.getUserVariables() 不可用。如果插件使用了 userVariables,需要在测试脚本中模拟:
// 在 require 插件之前模拟 env
global.env = {
getUserVariables: () => ({ cookie: '测试用cookie值' }),
os: 'win32',
appVersion: '0.0.1',
lang: 'zh-CN',
};
const plugin = require('./my-plugin.js');
.js 文件上传到仓库https://raw.githubusercontent.com/用户名/仓库名/分支/文件名.js)srcUrl 字段用户在 MusicFree 中通过 "从 URL 安装插件" 功能,粘贴插件的 raw 链接即可安装。
version 字段(遵循语义化版本,如 "0.0.1" → "0.0.2")throw,不要 return nullasync/await 可放心使用,?.、?? 在桌面版可用,但移动端可能需要 babel 转译User-Agent: okhttp/4.10.0),某些服务器可能因此返回异常,可手动设置 headers 覆盖本 Skill 的最新版本托管在官方仓库:https://github.com/maotoumao/musicfree-skills