Files
MusicServer/docs/source-simple.md
snowy-yang 96f6a7fe91 feat(source): 外部音源迁 src/ 并新增 source-simple 进程内直载
- 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)
2026-10-01 16:35:08 +08:00

4.4 KiB
Raw Permalink Blame History

外部音源 —— 简单 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 的死循环请求)由超时兜底放弃等待,脚本侧仍可能继续在后台运行