解密各平台加密音乐并转码增强音质
29.8/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/9/29 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
相关生态插件(非 dsh 原生,按其对应运行时安装)
git clone https://github.com/yousj666/dsh-audio-converter.git状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:待复验
是什么
生态应用
桌面端 / Web 外壳,不以 dsh plugin add 安装
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
更新放缓:最近一次提交在 37 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 119 处,展示前 108 条)
- 读取本地文件lib/index.js:194
if (!existsSync(p)) { out.push({ path: p, missing: true }); continue } - 读取本地文件lib/index.js:195
const st = statSync(p) - 读取本地文件lib/index.js:197
for (const f of readdirSync(p)) { - 读取本地文件lib/index.js:199
try { if (statSync(full).isFile() && ACCEPT_EXTS.includes(extname(f).toLowerCase())) out.push({ path: full }) } catch { } - 读取本地文件lib/index.js:324
const buf = readFileSync(f.path) - 写入本地文件lib/index.js:479
mkdirSync(dir, { recursive: true }) - 写入本地文件lib/index.js:483
writeFileSync(target, bytes) - 删除本地文件递归lib/index.js:556
try { rmSync(incomingDir(), { recursive: true, force: true }) } catch { } - 写入本地文件lib/index.js:614
mkdirSync(dir, { recursive: true }) - 写入本地文件lib/index.js:616
writeFileSync(join(dir, 'debug.log'), line, { flag: 'a' }) - 发起网络请求lib/client.js:105
return fetch(API + path, init).then(function (r) { - 发起网络请求lib/client.js:112
return fetch(API + '/upload?name=' + encodeURIComponent(file.name), { - 读取本地文件lib/pipeline.js:149
const buf = readFileSync(inputPath) - 写入本地文件lib/pipeline.js:159
try { mkdirSync(workDir, { recursive: true }) } catch { } - 写入本地文件lib/pipeline.js:177
writeFileSync(audioPath, out.audio) - 写入本地文件lib/pipeline.js:185
writeFileSync(coverPath, out.cover) - 写入本地文件lib/pipeline.js:212
writeFileSync(audioPath, out.audio) - 写入本地文件lib/pipeline.js:227
writeFileSync(audioPath, out.audio) - 写入本地文件lib/pipeline.js:242
writeFileSync(audioPath, out.audio) - 读取本地文件lib/pipeline.js:280
if (!existsSync(input)) return { ok: false, input, error: '文件不存在' } - 读取本地文件lib/pipeline.js:281
const sizeBytes = statSync(input).size - 读取本地文件lib/pipeline.js:327
outSizeBytes: statSync(outPath).size, - 读取本地文件lib/ffmpeg.js:52
if (!existsSync(base)) return [] - 读取本地文件lib/ffmpeg.js:55
for (const pkg of readdirSync(base)) { - 读取本地文件lib/ffmpeg.js:62
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return } - 执行外部命令来自配置/变量lib/ffmpeg.js:102
child = spawn(cmd, args, { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }) - 读取本地文件lib/ffmpeg.js:456
const hasCover = typeof cover === 'string' && cover.length > 0 && existsSync(cover) - 写入本地文件lib/ffmpeg.js:508
try { mkdirSync(dirname(output), { recursive: true }) } catch { } - 读取本地文件lib/ffmpeg.js:518
try { sizeBytes = existsSync(output) ? (await import('node:fs')).statSync(output).size : null } catch { } - 读取本地文件lib/ffmpeg.js:531
const hasCover = typeof cover === 'string' && cover.length > 0 && existsSync(cover) - 写入本地文件lib/ffmpeg.js:543
try { mkdirSync(dirname(output), { recursive: true }) } catch { } - 写入本地文件lib/ffmpeg.js:552
try { mkdirSync(dir, { recursive: true }) } catch { } - 读取本地文件lib/resources.js:80
return readFileSync(diskPath) - 读取本地文件lib/resources.js:88
return existsSync(diskPath) - 读取本地文件lib/resources.js:107
if (extractedFfmpeg && existsSync(extractedFfmpeg)) return extractedFfmpeg - 读取本地文件lib/resources.js:113
if (existsSync(target) && statSync(target).size > 1024 * 1024) { - 读取本地文件lib/resources.js:113
if (existsSync(target) && statSync(target).size > 1024 * 1024) { - 写入本地文件lib/resources.js:117
mkdirSync(dir, { recursive: true }) - 写入本地文件lib/resources.js:121
writeFileSync(tmp, buf) - 读取本地文件lib/resources.js:122
try { writeFileSync(target, readFileSync(tmp)) } catch { } - 写入本地文件lib/resources.js:122
try { writeFileSync(target, readFileSync(tmp)) } catch { } - 读取本地文件convert.mjs:95
if (!existsSync(full)) { files.push({ path: full, missing: true }); continue } - 读取本地文件convert.mjs:96
const st = statSync(full) - 读取本地文件convert.mjs:98
for (const f of readdirSync(full)) { - 读取本地文件convert.mjs:101
if (statSync(fp).isFile() && ACCEPT.has(extname(f).toLowerCase())) files.push({ path: fp }) - 读取本地文件convert.mjs:165
const det = detectFormat(readFileSync(f.path), f.path) - 写入本地文件convert.mjs:177
mkdirSync(outDir, { recursive: true }) - 读取本地文件diag-ncm.mjs:71
if (!existsSync(s.ncm)) { console.log(' 源文件不在,跳过'); continue } - 读取本地文件diag-ncm.mjs:73
const out = decryptNcm(readFileSync(s.ncm)) - 读取本地文件diag-ncm.mjs:87
if (existsSync(s.known)) { - 读取本地文件diag-ncm.mjs:88
const known = readFileSync(s.known) - 执行外部命令来自配置/变量exe/build.mjs:51
const c = spawn(cmd, args, { windowsHide: true, stdio: 'inherit', ...opts }) - 读取本地文件exe/build.mjs:68
if (!existsSync(FFMPEG_SRC)) { - 读取本地文件exe/build.mjs:72
if (!existsSync(KUGOU_KEY)) { - 读取本地文件exe/build.mjs:76
const ffSize = statSync(FFMPEG_SRC).size - 读取本地文件exe/build.mjs:77
const keySize = statSync(KUGOU_KEY).size - 读取本地文件exe/build.mjs:80
log(`node.exe ${mb(statSync(process.execPath).size)}`) - 读取本地文件exe/build.mjs:81
log(`预计 exe 约 ${mb(statSync(process.execPath).size + ffSize + keySize + 2 * 1048576)}`) - 写入本地文件exe/build.mjs:83
mkdirSync(DIST, { recursive: true }) - 写入本地文件exe/build.mjs:84
mkdirSync(TEMP, { recursive: true }) - 写入本地文件exe/build.mjs:112
writeFileSync(seaConfigPath, JSON.stringify({ - 删除本地文件exe/build.mjs:131
rmSync(outExe, { force: true }) - 写入本地文件exe/build.mjs:132
copyFileSync(process.execPath, outExe) - 执行外部命令来自配置/变量exe/build.mjs:149
const c = spawn(outExe, ['--help'], { windowsHide: true }) - 删除本地文件递归exe/build.mjs:168
try { rmSync(TEMP, { recursive: true, force: true }) } catch { } - 读取本地文件exe/main.mjs:53
if (!existsSync(full)) { files.push({ path: full, missing: true }); continue } - 读取本地文件exe/main.mjs:55
try { st = statSync(full) } catch { files.push({ path: full, missing: true }); continue } - 读取本地文件exe/main.mjs:58
try { entries = readdirSync(full) } catch { } - 读取本地文件exe/main.mjs:61
try { if (statSync(fp).isFile() && ACCEPT_SET.has(extname(f).toLowerCase())) files.push({ path: fp }) } catch { } - 写入本地文件exe/main.mjs:99
mkdirSync(outDir, { recursive: true }) - 写入本地文件exe/main.mjs:383
mkdirSync(incoming, { recursive: true }) - 写入本地文件exe/main.mjs:387
writeFileSync(target, bytes) - 写入本地文件exe/main.mjs:401
mkdirSync(outDir, { recursive: true }) - 执行外部命令固定命令exe/main.mjs:452
if (process.platform === 'win32') spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore', windowsHide: true }).unref() - 执行外部命令固定命令exe/main.mjs:453
else if (process.platform === 'darwin') spawn('open', [url], { detached: true, stdio: 'ignore' }).unref() - 执行外部命令固定命令exe/main.mjs:454
else spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref() - 读取本地文件test-client.mjs:12
const source = readFileSync(join(here, 'lib', 'client.js'), 'utf8') - 读取本地文件test-ffmpeg.mjs:46
check('ffprobe 也在', existsSync(ffprobePath()) || ffprobePath() === 'ffprobe') - 读取本地文件test-ffmpeg.mjs:74
if (!existsSync(ncmPath)) { - 读取本地文件test-ffmpeg.mjs:77
const dec = decryptNcm(readFileSync(ncmPath)) - 写入本地文件test-ffmpeg.mjs:79
writeFileSync(rawPath, dec.audio) - 写入本地文件test-ffmpeg.mjs:100
if (dec.cover) { writeFileSync(coverPath, dec.cover); console.log(' 封面已提取 ' + mb(dec.cover.length)) } - 读取本地文件test-ffmpeg.mjs:107
check(`转 ${fmt} 成功(${mb(r.sizeBytes)} → ${pi.codec})`, existsSync(out) && pi.codec !== null, - 读取本地文件test-ffmpeg.mjs:115
if (existsSync(flacOut)) { - 读取本地文件test-ffmpeg.mjs:324
if (ffOk9 && existsSync(ncm9)) { - 写入本地文件test-ffmpeg.mjs:327
writeFileSync(src9, dec9.audio) - 删除本地文件递归test-ffmpeg.mjs:360
try { rmSync(work, { recursive: true, force: true }) } catch { } - 读取本地文件test-host.mjs:141
check('文件真的写出来了', existsSync(up.payload?.path)) - 读取本地文件test-host.mjs:190
if (existsSync(srcNcm)) { - 读取本地文件test-host.mjs:207
if (existsSync(srcNcm) && st.payload.ffmpeg.ok) { - 读取本地文件test-host.mjs:220
check('输出文件存在', existsSync(res.output)) - 读取本地文件test-host.mjs:248
if (existsSync(srcNcm) && st.payload.ffmpeg.ok) { - 读取本地文件test-host.mjs:268
check('job 里带输出路径', typeof job.output === 'string' && existsSync(job.output)) - 删除本地文件递归test-host.mjs:291
try { rmSync(work, { recursive: true, force: true }) } catch { } - 读取本地文件test-ncm.mjs:205
if (!samples.some((s) => existsSync(s.ncm))) { - 读取本地文件test-ncm.mjs:214
if (!existsSync(s.ncm)) { console.log(' ⏭ 跳过(源文件不在): ' + s.ncm); continue } - 读取本地文件test-ncm.mjs:217
const src = readFileSync(s.ncm) - 读取本地文件test-ncm.mjs:241
if (existsSync(s.known)) { - 读取本地文件test-ncm.mjs:242
const known = readFileSync(s.known) - 读取本地文件test-pipeline.mjs:42
if (existsSync(srcNcm)) { - 读取本地文件test-pipeline.mjs:43
const buf = readFileSync(srcNcm) - 读取本地文件test-pipeline.mjs:126
if (!existsSync(srcNcm)) { - 读取本地文件test-pipeline.mjs:138
check('输出文件存在且非空', existsSync(r.output) && statSync(r.output).size > 1024) - 读取本地文件test-pipeline.mjs:138
check('输出文件存在且非空', existsSync(r.output) && statSync(r.output).size > 1024) - 读取本地文件test-pipeline.mjs:214
const all = existsSync(ncmDir) ? (await import('node:fs')).readdirSync(ncmDir).filter((f) => f.endsWith('.ncm')) : [] - 写入本地文件test-pipeline.mjs:252
writeFileSync(qmcPath, Buffer.concat([cipher, ekeyBuf, lenBuf])) - 写入本地文件test-pipeline.mjs:281
writeFileSync(stagPath, Buffer.concat([cipher, stagCsv, stagLen, Buffer.from('STag')])) - 删除本地文件递归test-pipeline.mjs:296
try { rmSync(work, { recursive: true, force: true }) } catch { }
扫描于 2026/11/2(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
依赖的 DSH / Cordis 模块
@deepseek-ai/cordis给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
功能亮点
- 解密多平台加密音乐
- 格式互转
- 音质增强
DSH 音频转换器:解密网易云 NCM / 酷我 KWM / 酷狗 KGM / QQ音乐 QMC,格式互转 + 音质增强(EBU R128 响度归一化、EQ、重采样、位深)。可打成 197MB 单文件 exe,零依赖。
README
由 DeepSeek 最新模型翻译生成dsh-audio-converter
音频转换器:解密各平台加密音乐,常规格式互转,并提供真实的音质增强。带拖拽界面。
拖进去,出来就是能播的文件。
网易云 .ncm ─┐ 酷我 .kwm ─┤ mp3 / flac ─┼─→ 解密 → 增强 → 转码 → 打标签 → 输出 m4a / wav ─┤ ogg / opus ─┘
加密格式支持情况(诚实版)
| 平台 | 扩展名 | 状态 | 说明 |
| 网易云音乐 | .ncm | ✅ 已支持 | 与已知正确结果逐字节一致(32 个断言验证) |
| 酷我音乐 | .kwm | ✅ 已支持 | 密钥恢复 + 容器嗅探验证(15 个断言) |
| 酷狗音乐 | .kgm .kgma .vpr | ✅ 已支持 | v1~v4 已实现(272 字节 MEND 表 + 17/16/272 三重异或),与独立参考实现跨语言对拍 |
| QQ音乐 v1(老格式) | .tkm .bkc | ✅ 已支持 | 128 字节公开静态密钥;含 0x7FFF 边界规则,已跨语言对拍 |
| QQ音乐 v2(mflac/mgg) | .mflac .mgg .qmcflac | ✅ 已支持 | 尾包内嵌 EKey 的(QTag / PcV1Legacy)直接解;已跨语言对拍 |
展开完整文档(README 剩余 1 节)
dsh-audio-converter 音频转换器:解密各平台加密音乐,常规格式互转,并提供真实的音质增强。带拖拽界面。 拖进去,出来就是能播的文件。 网易云 .ncm ─┐ 酷我 .kwm ─┤ mp3 / flac ─┼─→ 解密 → 增强 → 转码 → 打标签 → 输出 m4a / wav ─┤ ogg / opus ─┘ 加密格式支持情况(诚实版) | 平台 | 扩展名 | 状态 | 说明 | | 网易云音乐 | .ncm | ✅ 已支持 | 与已知正确结果逐字节一致(32 个断言验证) | | 酷我音乐 | .kwm | ✅ 已支持 | 密钥恢复 + 容器嗅探验证(15 个断言) | | 酷狗音乐 | .kgm .kgma .vpr | ✅ 已支持 | v1~v4 已实现(272 字节 MEND 表 + 17/16/272 三重异或),与独立参考实现跨语言对拍 | | QQ音乐 v1(老格式) | .tkm .bkc | ✅ 已支持 | 128 字节公开静态密钥;含 0x7FFF 边界规则,已跨语言对拍 | | QQ音乐 v2(mflac/mgg) | .mflac .mgg .qmcflac | ✅ 已支持 | 尾包内嵌 EKey 的(QTag / PcV1Legacy)直接解;已跨语言对拍 | | QQ音乐 v2(无内嵌密钥) | STag / MusicEx 尾包 | ⚠️ 需外部 EKey | 尾包里只有资源元数据、没有密钥,得从客户端数据库取 | | QQ音乐 v3(新版) | .mgg1/.mgg2/.mmp4 | ❌ 离线不可能 | 见下 | | 酷狗 KGG | 加密版本 5 | ❌ 不可能 | 密钥在客户端密钥库里 | | 虾米音乐 | .xm | ❌ 不可能 | 虾米已停止服务 | QMC v2 的四种尾包 QQ 音乐 v2 的密钥跟着文件走,藏在文件末尾的「尾包」里。四种形态: | 尾包 | 有 EKey? | 能不能离线解 | | QTag | ✅ 内嵌 | 能 —— 直接解 | | PcV1Legacy | ✅ 内嵌 | 能 —— 直接解 | | STag | ❌ 只有资源号 + mid | 需要外部 EKey | | MusicEx | ❌ 只有文件名 | 需要外部 EKey | 后两种的密钥存在客户端本地数据库(安卓端 player_process_db)里,文件本身不含。 工具和界面上都有 EKey 输入框,把密钥填进去就能解。 判断「能不能解」用尾包解析结果,不是看标记名。 一开始我按 STag/QTag 标记判定,把 STag 误判成「新版无解」——其实它和 QTag 一样都是 v2,区别只在尾包里有没有密钥。 为什么有两项是「不可能」而不是「还没做」 QQ 音乐新版采用动态密钥分发 + 设备指纹绑定: 播放请求 → 服务端验证账号与设备合法性 → 下发密钥 → 本地解密 密钥根本不下发到本地文件里。 所以拿到 .mgg1 / .mgg2 / .mmp4 之后,本地没有任何可用于解密的材料 —— 这不是实现难度问题,是设计上就不给。 与其假装支持然后吐一个损坏文件,不如直接说清楚。 界面和 audio_capabilities 工具都会明确告诉用户这一点,并给出可行替代方案(用客户端重新下载为普通格式,或在客户端内导出)。 音质增强:会做什么、不会做什么 会做(真实有效的处理) | 处理 | 效果 | 参数 | | 响度归一化 | 让音量一致,避免忽大忽小 | EBU R128,默认 -16 LUFS / -1.5 dBTP | | 夜间模式 | 压缩动态,小声也听得清 | acompressor ratio 4:1 | | 暖声 EQ | 减少刺耳感 | 200Hz +2dB / 4kHz -2dB | | 明亮 EQ | 提升清晰度 | 3kHz +3dB / 10kHz +2dB | | 人声突出 | 播客/人声向 | 1kHz +2dB / 300Hz -2dB | | 低频增强 | 补耳机低频 | 80Hz +5dB | | 高通 | 去超低频浑浊 | 30Hz | | 重采样 / 声道 | 匹配设备 | 任意采样率、单/立体声 | | 位深 | 匹配设备或做后续处理 | 16 / 24 / 32 位(只对 flac/wav 这类无损容器有意义) | 不会做 无法恢复有损编码已经丢掉的信息。 128kbps 的 mp3 转成 FLAC 不会变好听 —— 它只是把同样的信息用更大的容器装起来。 界面和工具描述里都明确写了这一点,不做「一键变无损」这种假承诺。 常规格式互转 mp3 · flac · m4a · aac · wav · ogg · opus 三档质量:体积优先 / 标准 / 质量优先。 装 dsh plugin --profile add dsh-audio-converter 或本地开发 dsh plugin --profile add link:/path/to/dsh-audio-converter 依赖 ffmpeg(必需)。程序会按「能不能跑起来」自动探测,不依赖任何固定路径: winget install Gyan.FFmpeg # Windows brew install ffmpeg # macOS apt install ffmpeg # Debian/Ubuntu ⚠️ 探测用的是「试跑 -version」而不是 existsSync —— 因为 WinGet 的应用执行别名(App Execution Alias)Node 的 existsSync 解析不了,而且 winget 有时会留下「清单还在、文件已删」的幽灵安装(本机就遇到了)。 详见知识库 P-017。 用 界面上用(推荐) 会话视图多一个 「音频转换」 页签,排在 「上下文」「定时任务」 后面(conversation.view,order 40)。 把文件拖进虚线框 → 选格式/质量/增强 → 点「开始转换」 - 拖拽 或 选择文件 - 任务走 job 模型,界面不卡;实时显示状态、耗时、输出体积 - 底部有诚实的能力矩阵,哪些能解、哪些不可能,一目了然 - 文件全程在本机处理,不上传任何服务器 让模型帮你转 「把 G:\CloudMusic\VipSongsDownload 里的 ncm 全转成 mp3」「看一下这个 .mgg 文件到底是什么格式,能不能解」「把这批 flac 做响度归一化,再转成 m4a」 三个工具: | 工具 | 作用 | | audio_convert | 转换/解密文件或整个目录,可指定格式、质量、增强预设 | | audio_inspect | 看一个文件到底是什么(按 magic 不按后缀)、能不能解 | | audio_capabilities | 查支持矩阵与 ffmpeg 状态,处理前先确认 | 设计说明 格式识别:先看 magic,再看后缀 有些人只是把 FLAC 改成了 .ncm 后缀。只看后缀会把普通文件当加密文件处理,白白报错。所以先按文件头识别: CTENFDAM → NCM 7C D5 32 EB … → KGM 05 28 BC 96 … → VPR fLaC / ID3 / OggS / ftyp / RIFF → 普通音频,无需解密 STag / QTag / musicex → QQ音乐新版 结果结构必须一致 processFile 有两条返回路径(只解密 / 解密+转码)。第一版里提前返回那条漏了 srcInfo 和 meta —— 调用方得按分支处理,很容易漏字段。 现在两条路径返回完全相同的结构,并且有测试盯着这条约束。 解密正确性怎么验证的 不是「自己加密再自己解密」 —— 那只证明两者互逆,证明不了跟真实格式一致。 - NCM:拿真实 .ncm 解密,跟一份已知正确的转换结果在去掉 ID3 标签之后逐字节比对 → 完全一致 - KWM:自造样本往返(5 轮随机内容)+ 三条密钥恢复路径 + 认不出时必须抛错 顺带一个坑:直接比整个文件的哈希会误判 —— 不同工具解密后会把封面重新嵌进 ID3,一个 1KB 的标签能变成 795KB,但音频载荷是一样的。 详见知识库 P-018。 测试 npm test # 一次跑完 8 个测试文件 node test-ncm.mjs # NCM 解密器 32 个断言 node test-kwm.mjs # KWM 解密器 15 个断言 node test-kgm-qmc.mjs # KGM + QMC v1 41 个断言(跨语言对拍) node test-qmc2.mjs # QMC v2 46 个断言(跨语言对拍) node test-ffmpeg.mjs # ffmpeg 封装 75 个断言 node test-pipeline.mjs # 格式识别与管线 57 个断言 node test-host.mjs # host 半边 60 个断言 node test-client.mjs # 浏览器半边 34 个断言 合计 360 个断言。 不需要装进 DSH、也不需要浏览器(除 host/client 用 mock ctx)。 只需 Node ≥ 22.19,没有构建步骤。 ffmpeg 可选 —— 没装的话用到它的测试会跳过。 test-ncm.mjs 的第 4 节需要你自己的 .ncm 素材(用环境变量指定,见 CONTRIBUTING.md);没设就整节跳过,其余断言照常跑。 亮点: - 真实素材 + 外部锚点:真 .ncm 文件 → 与已知正确结果在去掉 ID3 之后逐字节比对 - 跨语言对拍:黄金值由一份独立写的 Python 参考实现算出(不 import 原项目), JS 侧翻译错了就对不上。QMC 的 0x7FFF 边界、TEA 的 float32 语义、 Map/RC4 的流密码全都靠这个办法验证 - 实测增强效果:用 ffmpeg 的 volumedetect 量处理前后的响度,确认不是空转 - 回读产物校验:转完再探测一遍输出,比对采样率/时长/编码 —— 采样率被滤镜悄悄改掉就是这么抓到的 - 回退路径也测:没有 ffprobe 时解析 ffmpeg -i 的 stderr,22 个断言确保结果和 ffprobe 一致 - 安全栅栏:非回环 Host / 跨站发起一律 403;上传时的路径穿越防护 - 诚实性断言(源码级):界面必须写明「无法恢复有损信息」「离线做不到」 已知边界 - QMC v2 的 STag / MusicEx 尾包不含密钥 —— 需要用界面上的 EKey 输入框手动提供 - 酷狗 KGG(加密版本 5)不支持 —— 密钥在客户端密钥库 - QQ音乐 v3(新版)离线无解 —— 不是实现问题,是密钥不下发到本地 - 酷狗公钥表是截断版(8 MB,覆盖 128 MB 音频)。要处理更大的文件,跑 node scripts/expand-kugou-key.mjs 生成完整表(69.77 MB) - 上传文件上限 512MB(拖拽走 HTTP,超大文件建议用工具传路径) - 批量处理是串行的(避免一次转几十首把机器打满);一次提交几十首会排队 - 解密后立即写入 ffmpeg 能读的临时文件,转换完不会自动清理临时目录 - QMC/KGM 没有官方测试素材 —— 验证靠「自造合法样本 + 跨语言对拍」,不是靠真实文件。如果你手上有真实文件跑出问题,欢迎反馈 - Windows 路径分隔符按反斜杠处理;其他平台未实测 三种用法 ① DSH 插件(本项目的主体) 会话视图里多一个「音频转换」页签(conversation.view,order 40),拖文件进去就行。 同时给模型注册 3 个工具,可以直接说「把某个文件夹里的 ncm 全转成 mp3」。 ② 命令行(需要 Node + ffmpeg) node convert.mjs [--format mp3] [--enhance loudness] [--out 目录] node convert.mjs --list # 看支持哪些格式 node convert.mjs --dry # 只识别格式,不真转 Windows 上也可以双击 convert.bat,或者把文件拖到它上面。 ③ 单文件 exe(不需要任何依赖) npm run build-exe 打出一个约 197 MB 的独立 exe(内嵌 Node + ffmpeg): - 拖文件上去 → 直接转成 mp3 - 双击 → 本地网页设置界面,能选格式/质量/位深/增强 拷到任何 Windows 上都能跑,不需要装 Node、不需要装 ffmpeg。 体积构成:node 88 MB + ffmpeg 100 MB + 酷狗公钥表 8 MB。 不带 ffprobe —— 它是另一个约 100 MB 的静态二进制,而 probe() 已经能在没有它时解析 ffmpeg -i 的 stderr(22 个断言盯着这条路径)。 文件 lib/crypto/ncm.js NCM 解密器(AES-128-ECB + RC4 变体) lib/crypto/kwm.js KWM 解密器(32 字节循环 XOR + 密钥恢复) lib/crypto/kgm.js KGM 解密器(MEND 表 + 三重异或) lib/crypto/qmc.js QMC v1(128 字节静态密钥 + 0x7FFF 边界规则) lib/crypto/qmc2.js QMC v2(尾包解析 + TEA + Map/RC4 流密码) lib/crypto/assets/kugou_key.bin 酷狗公钥表(8 MB,覆盖 128 MB 音频) lib/crypto/assets/kugou_key.xz 同一张表的完整版压缩源(94 KB) lib/ffmpeg.js ffmpeg 封装:探测 / 转码 / 增强 / 打标签 lib/pipeline.js 格式识别 → 解密 → 转换 → 增强 的调度 lib/resources.js 资源解析:磁盘文件 / exe 内嵌 两种模式 lib/index.js host 半边:3 个工具 + 5 条 HTTP 路由 + job 队列 lib/client.js 浏览器半边:拖拽页签(手写,无构建) convert.mjs 命令行入口 convert.bat Windows 拖拽入口 exe/main.mjs 单文件 exe 的入口(拖拽 + 本地界面) exe/build.mjs 打包脚本(esbuild → SEA → postject) scripts/expand-kugou-key.mjs 从 .xz 生成完整公钥表 test-.mjs 8 个测试文件,360 个断言 diag-ncm.mjs NCM 诊断工具(排查"解出来对不对"用) 参与开发 见 CONTRIBUTING.md。两条硬规矩: 1. 解密正确性必须用外部锚点证明 —— 自己加密再自己解密只能证明互逆 2. 做不到的如实标注 —— 宁可报错也不要吐出损坏文件 许可 MIT —— 见 LICENSE。 解密算法的参考资料、酷狗公钥表的来源与性质、FFmpeg 的许可说明,都写在 THIRD-PARTY-NOTICES.md 里。 免责声明 本项目仅用于个人已购音乐的格式转换 —— 把你花钱买的歌从专有容器里拿出来,在你自己拥有的设备上播放。请勿用于传播受版权保护的内容。
预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
yousj666/dsh-audio-converter
解密各平台加密音乐并转码增强音质
相关插件 · 平台集成
智能体工程课walkinglabs/learn-harness-engineering
一门项目制课程,教你搭建环境、管理状态、做验证、加控制机制,让 AI 编程智能体稳定干活。
★ 21,155已汉化DSH
71.7
视频翻译配音liuzhao1225/YouDub-webui
开源视频本地化工作台:导入视频,识别、翻译并逐句克隆原声,生成字幕和配音成片。
★ 5,778已汉化Cordis
71.4
失败经验库Ikalus1988/MisakaNet
📚 零依赖、由 git 支撑的微课程库,让 AI 智能体异步分享并检索经过验证的调试经验。| https://misakanet.org
★ 550已汉化DSH
71.2
桌面智能体工作区freestylefly/wesight
WeSight 是开源桌面控制台,面向本地 AI 智能体。它帮你安装或复用 Claude…
★ 933已汉化MCP
71
IM 机器人接入xmanrui/dsh-im
让 DeepSeek Harness 触手可及 Connecting DeepSeek Harness
★ 3,110已汉化DeepSeek 客户端
70.9
中国平台研究引擎Jesseovo/last30days-skill-cn
给 AI Agent 的中文互联网研究 Skill:检索 8 大平台最近 30…
★ 2,031已汉化DSH
70.7