Files
MusicServer/CODE-AUDIT.md
T
snowy-yang dd28ba157c docs: 文档与实现对齐,并收录全量代码检查报告
- README:平台能力表按实际能力位修正(qs 歌单/专辑改 —、kw/kg/tx/wy 的
  parse 改 ✅、qt/lr 封面改 ✅),接口表补 album 分页位、parse/playlist
  支持平台、user-playlist 仅 tx/wy、DELETE 登录端点、qs 扫码方式;
  项目结构补 http/ 下 4 个文件与 routes-login 的 bili/lr
- docs/API.md:整篇改为指向 /openapi.json 的速查版,把唯一的接口事实
  来源收敛到 JSON Schema;平台数按实际 15 个更新
- CODE-AUDIT.md:新增,记录 2026-10-03 全量检查的 P0/P1/P2 与文档不一致项,
  顶部标注修复状态(已修 P0/P1,未动项明确列出)

为什么改写而不是逐条打补丁:文档与实现双向失配的地方太多,逐条修补会留下
自相矛盾的段落;改成「能力以 /sources 与 /openapi.json 为准,文档只做索引」
后,漂移的结构性来源就被消除了。
2026-10-03 00:27:30 +08:00

19 KiB
Raw Blame History

MusicServer 全量代码检查报告

检查时间:2026-10-03 修复状态(复查后更新):P0-1 ~ P0-3、P1-4 ~ P1-16 全部已修复(含测试隔离与 isolated-vm 原生构建); 第五节「文档与代码不一致」已按 /sources 与路由实现逐条校准;第八节列出的未复核线索与第四节 P2 项未动,留待后续。 各条的修复要点见下文对应条目。 检查范围:src/(15 文件 / 3,103 行)、sdk/(173 文件 / 21,919 行)、test/(2 文件 / 1,081 行),共约 26,100 行 TypeScript

检查方法:

  1. 静态门禁:tsc、tsc --noUnusedLocals --noUnusedParameters、离线音源测试、冒烟测试
  2. 全量模式扫描:any 滥用、@ts-ignore、TODO/FIXME、调试输出、死代码、硬编码敏感值
  3. 四组分区深度审查:HTTP 层 / SDK 核心(认证+取流)/ SDK 平台模块 / 测试与文档
  4. 针对性复现验证:对判定为 P0 的问题编写最小复现脚本确认

一、基线状态

检查项 结果
pnpm typecheck(tsc strict) ✅ 通过
未使用符号(--noUnusedLocals) ⚠️ 26 处
离线测试 test/source-simple-test.ts ✅ 8/8 通过
冒烟测试 test/smoke-test.ts ⚠️ 不稳定:一次 110/111,一次直接崩溃(见 P1-4)
@ts-ignore / @ts-expect-error ✅ 0 处(纪律良好)
TODO / FIXME / HACK ✅ 0 处
lint / CI 配置 ❌ 均无(质量门禁仅 tsc + 冒烟测试)

二、P0 阻断级

P0-1 POST /login/lr 路由缺失 —— 懒人听书取流 100% 不可用

证据链:

  • README.md:86 与 README.md:105 明确承诺:懒人听书需 POST /login/lr 账密登录,且「取流必须登录(免费章节同样要求登录态)」
  • sdk/lr/auth.ts:12 已完整实现 login()(base64 密码 + 会话 Cookie 持久化)
  • 但 sdk/lr/index.ts:53 只写了 export { status } from './auth.ts' —— 没有导出 login
  • src/routes-login.ts 注册的 28 条路由中没有任何 lr 相关条目(逐条列举验证)

影响:sdk 层实现了登录能力,却没有任何 HTTP 入口。而 lr 取流强制要求登录态 → 该平台的取流功能完全无法使用,README 的承诺是空头支票。

修复:sdk/lr/index.ts 补导出 login;src/routes-login.ts 参照 /login/xm 补 POST /login/lr(body: {account, password})与 DELETE /login/lr;/login/status 加入 lr 字段。


P0-2 咪咕解密流异常逃逸 —— 可导致整个服务进程崩溃

证据链:

  • src/http/migu-stream.ts:88-92:res.on('data', chunk => { ...; onChunk(plain, offset) }) —— onChunk 在 EventEmitter 回调中同步调用
  • src/routes.ts:317:该回调内部有 throw new Error('解密结果不是有效音频')
  • 外层 src/routes.ts:311-324 的 try/catch 只能捕获 Promise 链上的 reject,捕不到 EventEmitter 回调抛出的异常
  • 全项目没有任何 uncaughtException / unhandledRejection 处理器(grep 无匹配)

复现验证(tmp/repro-migu-uncaught.mjs,已实测):

❌ uncaughtException 逃逸(进程会崩溃): 解密结果不是有效音频
exit code 9

影响:当咪咕返回的密流与票据 fileKey 不匹配(解密结果非法),或客户端中途断开导致 reply.raw.write 抛错时,异常逃逸 → 整个 Node 进程崩溃,所有在线用户连接同时中断。这是一个远程可触发的单点崩溃。

修复:onChunk 内不要 throw,改为置错误标志 + res.destroy() / reply.raw.destroy();或让 pipeDecryptedMiguStream 把 transform/onChunk 的调用包进 try/catch 并 reject。另建议补一个全局 uncaughtException 兜底(至少记日志后优雅退出,而非静默崩溃)。


P0-3 登录凭据明文落盘且权限过宽

证据链:

  • sdk/auth/store.ts:88:writeFileSync(DATA_FILE, JSON.stringify(data, null, 2)) —— 明文 JSON,且未传 { mode: 0o600 }
  • 实际文件:data/auth.json 权限 -rw-r--r--(0644),内容为各平台 cookie/token 明文
  • 文件覆盖 wy / kg / tx / xm / me / lr / bili / qs 等全部平台凭据

影响:任何能读取该目录的本地用户或进程可直接窃取全部平台登录态(等价于账号取流权限)。Windows 上 POSIX 位不完全生效,风险取决于 ACL,但明文存储这一点在任意平台都成立。

修复:writeFileSync(..., { mode: 0o600 })(Windows 侧另设 ACL);如需更高保障再考虑加密存储(密钥来自环境变量)。


三、P1 应修

P1-4 冒烟测试异常路径不清理子实例 → 僵尸进程累积

证据:

  • test/smoke-test.ts:81-110 startLocalServer() 超时后 proc.kill() 再 reject
  • 主实例之外还有两个额外实例:authServer(:835)、rlServer(:887)
  • :898-899 的 rlServer.kill() / authServer.kill() 位于顶层顺序执行,不在 try/finally 内 —— 一旦中间抛错就不会执行
  • :911-915 顶层只清理了 mainServer(localServer),不涉及额外实例

实测后果:当前机器上有 6 个 MusicServer 实例在监听:16688(常驻)+ 16690/16691/16692/16693/16790(测试泄漏,连续端口说明是多轮泄漏累积)。本次审查中冒烟测试崩溃后即留下新实例。

影响:每次测试异常就泄漏一个监听端口的 node 进程,长期累积占用内存/端口,且会干扰后续测试判定。

修复:用 try/finally 或 process.on('exit') 统一 kill 所有已启动实例。


P1-5 冒烟测试启动超时 10s 硬编码,且失败时无任何诊断信息

证据:test/smoke-test.ts:91-94

const timer = setTimeout(() => {
  proc.kill()
  reject(new Error('启动本地服务器超时'))   // ← 未附 stdout
}, 10000)

stdout 变量已收集子进程输出(:87-89),但 reject 时完全丢弃。

影响:本次审查中同一份代码一次通过(110/111)、一次崩溃,说明该超时在并发/负载下很脆弱 —— 属于典型的 flaky 测试。而失败信息只有一句「启动本地服务器超时」,无法判断是端口被占、TS 报错还是单纯慢。

修复:超时提到 30s;错误信息附带 stdout 尾部;区分「进程已退出」与「进程存活但未就绪」。


P1-6 /song-list/:source 的 ?limit= 被文档承诺却静默忽略

证据:

  • src/routes.ts:450:querystring schema 声明了 limit: pLimit
  • src/routes.ts:436:handler 签名为 (source, page, sortId, tagId) —— 没有 limit 参数
  • src/routes.ts:458:调用时也未传 request.query.limit

影响:OpenAPI 文档告诉客户端可以传 ?limit=,实际被丢弃 —— API 契约与实现不符。

修复:要么把 limit 透传到 songList.getList(),要么从 schema 移除。


P1-7 凭据文件写入非原子 + 解析失败静默清空

证据(sdk/auth/store.ts):

  • :84-89 save() 直接 writeFileSync 覆盖原文件,无 temp+rename。写入过程中进程被杀 → 文件损坏
  • :73-82 load() 用 catch { cache = {} } 静默吞掉 JSON 解析错误;随后任何 setCredentials() 都会用空数据覆盖重写损坏文件 → 所有平台凭据一次性丢失,且无任何日志
  • 无跨进程锁:当前环境并存 6 个实例,写操作会互相覆盖

修复:写临时文件 + rename() 原子替换;解析失败时先备份原文件再重置,并打印告警。


P1-8 过期/错误注释误导维护者

证据:sdk/auth/store.ts:3

// 注意:store.js 为 tsc 就地发射产物(不入库);改逻辑只改本文件,然后 pnpm build

但:tsconfig.json 已是 noEmit: true,package.json 没有 build 脚本,项目已全面改为「无编译产物、改完即跑」。磁盘上也确认无 .js 残留。

影响:按注释执行 pnpm build 会失败,且误导开发者以为存在发射产物。

修复:删掉该行注释。


P1-9 酷狗 krc 歌词解码:Promise 永不 settle,请求挂死

证据:sdk/kg/krcDecode.ts:9-10

const decodeLyric = str => new Promise((resolve, reject) => {
  if (!str.length) return          // ← 既未 resolve 也未 reject

return 只结束了执行器函数,Promise 永久停在 pending。

影响:上游返回空 krc 歌词时,调用方的 await 永久挂起 —— 连接与内存无法释放,客户端只能等自己超时。

修复:if (!str.length) return resolve('')


P1-10 酷狗歌词重试调错方法

证据:sdk/kg/lyric.ts:72 —— 在 getLyricDownload(id, accessKey, fmt, tryNum) 内部重试时调用的是

let tryRequestObj = (this.getLyric as any)(id, accessKey, fmt, ++tryNum)

应为 this.getLyricDownload(...)。getLyric 与 getLyricDownload 参数语义不同,且 as any 恰好掩盖了这个错误。

影响:HTTP 非 200 时的重试实际走了另一个方法,重试逻辑失效(还会掩盖类型错误)。

修复:改为 this.getLyricDownload(id, accessKey, fmt, ++tryNum);顺带移除 as any。


P1-11 酷我歌单详情:重试参数错位(序号被当成页码)

证据:

  • sdk/kw/songList.ts:196:return this.getListDetail(id, ++tryNum)
  • sdk/kw/songList.ts:373:getListDetail(id, page, retryNum = 0) —— 第二个参数是 page

同一文件 :175、:206 都正确写成 getListDetail(id, page, ++tryNum),唯独 :196 漏了 page。

影响:重试时请求的页码被替换成 tryNum(1/2/3…),而不是用户请求的页;同时重试计数没有真正传递。

修复:补上 page,与 :206 对齐。


P1-12 酷我歌单详情 URL 模板多余 }

证据:sdk/kw/songList.ts:203

...&pid=${id}&pn=${page - 1}}&rn=${this.limit_song}&...
                              ↑ 多了一个右花括号

影响:请求 URL 携带非法字符,目前依赖上游容错;上游一旦收紧解析该接口即失效。

修复:删掉多余的 }。


P1-13 冒烟测试未做环境隔离:取流用例实际走外部音源,且会改写真实凭据

证据:

  • test/smoke-test.ts:84 以 env: { ...process.env, PORT, ...envExtra } 启动子实例 —— 完整继承当前进程环境
  • .env:1 为 SOURCE_ENABLED=true → 主实例以音源模式启动,/url 用例优先走 sources/ 脚本链
  • 子实例工作目录即项目根,data/auth.json 是真实凭据文件(无测试专用路径)

影响:① /url 用例并未验证 SDK 官方直连通道,SDK 侧回归可被外部音源掩盖(测试假通过);② /login/refresh 等用例会读写真实 data/auth.json —— 本地跑一次测试就可能覆盖真实登录态。

修复:测试显式传 SOURCE_ENABLED: '';新增 DATA_DIR 环境变量让测试实例使用独立数据文件。


P1-14 pnpm-workspace.yaml 使用非官方键 → isolated-vm 沙箱实际不可用

证据:

  • pnpm-workspace.yaml 全文仅两行:allowBuilds: / isolated-vm: true
  • pnpm --version = 12.4.1;放行构建脚本的字段应为 onlyBuiltDependencies,allowBuilds 不生效
  • node_modules/isolated-vm 目录存在,但 out/*.node 不存在 → 原生模块从未构建
  • 离线测试实测输出:未安装 isolated-vm,无法启用隔离执行音源…切换下一候选脚本

影响:src/source-ivm.ts 的「硬隔离沙箱」在部署时静默失效 —— 社区 .js 音源脚本全部被跳过,而 README 把它描述为可用的第一类音源。安全隔离能力名存实亡,且无显著告警。

修复:改为 onlyBuiltDependencies: [isolated-vm] 并确认构建产物就位;ivm 不可用时应在启动日志告警。


P1-15 冒烟测试断言游离在 test() 之外,抛错会跳过清理

证据:test/smoke-test.ts:855-858 的「6 个路径均须 401」断言写成裸 for 循环置于顶层,而对应的 await test(...) 在 :859。任一路径返回非 401 即顶层抛错。

影响:抛错直接终止脚本,跳过结果汇总与末尾的实例清理 —— 与 P1-4 同源,都会留下孤儿 node 进程。

修复:把断言包进 test(),或统一用 try/finally 收尾。


P1-16 .claude/settings.local.json 已入库且含本机绝对路径

证据:git ls-files .claude/ 返回 .claude/settings.local.json;内容含 C:\Users\Administrator\.claude\projects\d--MusicServer\memory 等本机路径。

影响:把本机目录结构与本地工具配置写进(README 声明为公开的)仓库,对他人无意义。

修复:git rm --cached .claude/settings.local.json,并把 .claude/ 加入 .gitignore。


四、P2 可维护性与卫生

类别 证据 说明
注释掉的死代码 sdk/wy/lyric.ts:49-51,240、sdk/kw/album.ts:242-246、sdk/kg/songList.ts:75-109,778、sdk/kw/index.ts:33-37 等 100+ 处 大量整段注释代码(含被注释的 console.log 调试行),建议清理
裸 console.log(err) 吞错 sdk/wy/songList.ts:103、sdk/wy/leaderboard.ts:188、sdk/kw/lyric.ts:142(还会打印完整歌词)、sdk/kg/songList.ts:279、sdk/kg/util.ts:35、sdk/tx/utils/index.ts:39、sdk/utils.ts:23 吞掉异常且无上下文,排障困难
未使用符号 26 处 sdk/index.ts(3)、sdk/tx/leaderboard.ts(2)、sdk/lr/index.ts(2)、sdk/joox/parse.ts(2)、sdk/apis/migu.ts(2) 等 tsc --noUnusedLocals 可复现
.gitignore 漏 .workbuddy/ .gitignore 内容 vs git status 显示 ?? .workbuddy/ 工作记忆目录会被 git add . 收进仓库
tmp/ 占 297 MB / 4,992 文件 内含 1753 js / 720 ts / 339 py / 287 tsx / 122 vue 已 gitignore,但混有大量与本项目无关的代码样本,建议清理磁盘
无 lint / 无 CI .eslintrc/.prettierrc/.github 均不存在 建议引入 ESLint + 一条 CI 跑类型检查与离线测试

五、文档与代码不一致清单

文档位置 文档声称 代码实际
README.md:86、:105 POST /login/lr 存在 不存在(P0-1)
README.md:20(qs 行) 歌单 ✅、专辑 ✅ sdk/qs/index.ts 无 album/playlist 方法,/sources 能力位也无
README.md:20 vs :66 内部自相矛盾 :20 给 qs 专辑打勾,:66 的专辑详情平台列表却不含 qs
README.md:71 parse 支持 5sing/apple/bili/joox/qian/migu 实际还支持 kw / kg / tx / wy(sdk/{kw,kg,tx,wy}/parse.ts 存在,/sources 已暴露 parse 位)
README.md:68 歌单分类仅 migu/qian/joox/apple 实际 kw / kg / tx / wy 也有 playlistCategories 能力位
README.md 登录接口表 只列了 POST 登录 实际还有 7 个 DELETE /login/{wy,kg,tx,qs,xm,me,bili} 退出端点未文档化
test/smoke-test.ts:642 /parse/song/kw 期望 404 实际 200(kw parse 已实现)→ 导致冒烟测试唯一的失败项
sdk/auth/store.ts:3 改完需 pnpm build 无 build 脚本,项目无编译产物(P1-8)
README.md:24 bili 音质 64k/128k/192k /sources 实际为 64k/128k/320k/flac/flac24bit
README.md:65 /album/:source/:id?page=1(query 分页) 分页是 path 段 /album/{source}/{id}/{page}
README.md:29-30 qt / lr 封面标「—」 /sources 显示两者 pic=true(实际支持)
README.md:72 个人歌单支持 kg/tx/wy/joox/bili/apple 实际仅 tx / wy
README.md:21-26 migu/qian/joox/apple/5sing 登录列标「cookie」 均没有 /login/{migu,qian,joox,apple,5sing} 路由(与 P0-1 同类问题)
docs/API.md 与 README 同源的平台/接口描述 同样滞后(parse 支持平台、user-playlist 支持集等)

六、环境与仓库卫生

  • 6 个 MusicServer 实例在监听:16688(常驻)、16690~16693、16790(测试泄漏,见 P1-4)。建议清理泄漏实例,16688 请确认是否为常用常驻服务后再决定。
  • 未追踪且会入库的目录:signer/、tools/、.workbuddy/、scripts/qs-local-login.mjs 以及大量新增的 sdk/** 模块(5sing/apple/bili/joox/migu/qian/kw·kg·tx·wy 的 parse/playlist 等)。
    • signer/server.mjs 本身实现规范(强制 QS_SIGNER_TOKEN、设备号脱敏、敏感头不落盘),入库无碍;
    • tools/mssdk-sign/ 含 .exe 与逆向笔记(REVERSE.md),是否入库请自行判断;
    • .workbuddy/ 建议加进 .gitignore。
  • .gitignore 已正确覆盖:node_modules/、sources/、data/、tmp/、*.log、soda-mssdk/、.env。

七、建议修复顺序

  1. P0-1(lr 登录)—— 一行导出 + 一个路由,成本极低,收益是解锁整个平台
  2. P0-2(migu 崩溃)—— 改 3 行错误处理 + 加全局兜底,消除远程可触发的进程崩溃
  3. P1-4 / P1-5(测试稳定性)—— 不修则后续每次 CI/回归都可能留下僵尸进程并产生假失败
  4. P1-8 / 第五节文档(文档同步)—— 顺手即可,避免误导
  5. P0-3 / P1-7(凭据存储)—— 视部署环境决定优先级
  6. P1-6、P2 各项按需清理

八、附:尚未逐条复核的线索

以下来自分区审查组的摘要,本轮未逐条复核到行号,建议作为下一轮排查入口。

SDK 核心组(sdk/index.ts、sdk/types.ts、sdk/common/request.ts、sdk/auth/*、sdk/apis/*):

  • wy 首次登录时账号资料获取失败(cookie 时序问题)
  • 请求取消语义失效;migu 候选链的多次探测请求不可取消
  • qs 解密时把整段音频读入内存(未走流式)
  • Quality 类型契约不一致:apple 实际返回 preview,但该值不在联合类型内
  • 各平台 cancelHttp 实现不一致;部分平台把 token 拼进 GET query

SDK 平台模块组(本轮已核实 4 条,见 P1-9 ~ P1-12)其余线索:

  • sdk/migu/util.ts:375-378 解密失败时把函数引用直接拼进异常消息
  • sdk/joox/ 单曲接口未校验上游 errorCode(错误探针外泄、响应未解析)
  • sdk/qian/util.ts:82 附近三个导出函数名与 JSDoc 描述完全对调
  • sdk/bili/util.ts 内约 6 处结构相同的 AJAX 重试代码

跨文件重复逻辑:kw / kg / tx / wy 的 parse.ts 与 playlist.ts 存在大段相似实现,可考虑抽出公共解析工具。

报告前三节的 P0 与 P1-4 ~ P1-12 均有明确代码位置与实际验证依据(其中 P0-2 附最小复现脚本);本节线索建议复核后再动手。第四组「测试与文档」专项审查仍在进行,其领域(测试有效性、覆盖缺口、文档一致性)本报告已通过独立核对覆盖主要结论。