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

103 lines
4.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 外部音源 —— 简单 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/` 目录(不入库),按**文件名混排**成候选链(顺序即优先级),失败自动切换下一个。
## 脚本契约
```js
// 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 为实际音质,用于音质回退标注)
抛错即本次取流失败,由候选链切换下一候选,不影响服务器。
## 示例
### 最小示例
```js
// 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 并透传取消信号
```js
// 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
}
```
### 按平台区分逻辑
```js
// 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 加载器](source-ivm.md)
- 仅接入**取流**(getMusicUrl);歌词、封面、榜单等能力始终走内置实现
- 脚本内部自起的长任务(如忽略 signal 的死循环请求)由超时兜底放弃等待,脚本侧仍可能继续在后台运行