- apis/source(.ivm).ts → src/source(.ivm).ts(音源属服务端职责,不入 SDK) - 新增 src/source-simple.ts:sources/*.mjs 进程内直载(getMusicUrl 极简契约), 与 *.js 的 IVM 沙箱(globalThis.lx 契约)按扩展名混排候选链,失败降权冷却 - server 挂载决策:SOURCE_ENABLED 非 0/false 启用,/url 取流分流到外部音源; 音源模式下登录态不参与取流,自动续期不启动(手动 /login/refresh 仍可用) - 新增 src/env.ts(.env 最先加载)与 test/source-simple-test.ts(并入 pnpm test)
4.4 KiB
4.4 KiB
外部音源 —— 简单 API 脚本(.mjs)
社区音源脚本之外的第二种脚本接入:脚本就是普通 Node ES 模块,只需导出一个 getMusicUrl 函数,无需 preload API 契约(lx.on / lx.request / crypto 桥接…)。适合自己写的取流脚本。
两种脚本对比
社区音源脚本(.js) |
简单 API 脚本(.mjs) |
|
|---|---|---|
| 加载器 | source-ivm.ts,isolated-vm 硬隔离沙箱 |
source-simple.ts,进程内直接 import |
| 脚本契约 | lx.on('request') + lx.send('inited') 全套 preload API |
一个 getMusicUrl 函数 |
| 能力 | 沙箱内桥接的 http/crypto/zlib | Node 全部能力(fetch / import 任意包) |
| 适用 | 来源不明的第三方混淆脚本 | 自己写、自己审计过的脚本 |
两种脚本都放 sources/ 目录(不入库),按文件名混排成候选链(顺序即优先级),失败自动切换下一个。
脚本契约
// sources/99-my-api.mjs
export const sources = ['kw', 'kg', 'tx', 'wy'] // 可选,默认 kw/kg/tx/wy 四平台
export const timeout = 15000 // 可选,单次取流超时 ms(默认 15000)
export async function getMusicUrl({ source, id, songInfo, quality, signal }) {
// ...你的取流逻辑
return 'https://…' // 播放地址字符串,或 { type, url }
}
getMusicUrl 参数:
| 参数 | 说明 |
|---|---|
source |
平台 id:kw / kg / tx / wy(或脚本自己声明的) |
id |
歌曲 id(已归一:songmid / hash / copyrightId 取第一个非空值) |
songInfo |
原始歌曲信息对象(含 songmid/hash 等原始字段) |
quality |
音质:128k / 320k / flac / flac24bit |
signal |
取消/超时信号(AbortSignal)——调用方取消或超时都会触发;fetch(url, { signal }) 透传即可支持取消 |
返回值:
- 字符串:播放地址(最简单)
{ type, url }:对象(type 为实际音质,用于音质回退标注)
抛错即本次取流失败,由候选链切换下一候选,不影响服务器。
示例
最小示例
// sources/99-demo.mjs
export async function getMusicUrl({ source, id, quality }) {
return `https://your-api.example/get?source=${source}&id=${id}&quality=${quality}`
}
调用 JSON API 并透传取消信号
// sources/98-my-api.mjs
const API = 'https://your-api.example'
export async function getMusicUrl({ source, id, quality, signal }) {
const res = await fetch(`${API}/url?source=${source}&id=${id}&quality=${quality}`, { signal })
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const { code, msg, data } = await res.json()
if (code !== 0) throw new Error(msg || `code=${code}`)
return data.url
}
按平台区分逻辑
// sources/97-multi.mjs
export const sources = ['kw', 'kg'] // 只接入 kw/kg,其余平台走候选链中的其它脚本
export async function getMusicUrl({ source, id, quality }) {
if (source === 'kw') return kwLogic(id, quality)
return kgLogic(id, quality)
}
候选链行为
- 顺序 = 文件名排序(
.js与.mjs混排),如01-api.js优先于99-my-api.mjs - 某脚本的平台连续失败 3 次 → 该脚本在该平台降权冷却 5 分钟(沉到候选末尾,恢复后自动探测)
- 单脚本无处可切时不做降权,行为与直连失败一致(路由 502)
SOURCE_SCRIPT_PATH指定单个社区脚本(.js)时跳过目录扫描,但.mjs仍参与候选链
启用方式与调试
- 环境变量
SOURCE_ENABLED=1(任意非0/false值)启用外部音源;不设置则走内置直连。音源模式下登录态不参与取流,登录自动续期不会启动 pnpm dev热重载已监听sources/目录,改脚本保存即自动重启生效- 日志前缀
[source-simple]:加载(平台列表)、每次取流(▶ 开始 / ✔ 完成 / ✖ 失败,含耗时) - 离线测试:
node test/source-simple-test.ts(已并入pnpm test)
边界
- 无沙箱:脚本运行在服务器进程内,可访问 fs/process 等——只放自己审计过的代码;不可信脚本用
.js走 isolated-vm 加载器 - 仅接入取流(getMusicUrl);歌词、封面、榜单等能力始终走内置实现
- 脚本内部自起的长任务(如忽略 signal 的死循环请求)由超时兜底放弃等待,脚本侧仍可能继续在后台运行