17 Commits
Author SHA1 Message Date
saopig1andClaude Opus 5 03ffd09d36 fix(setup): 按 Node ABI 校验并自动修复原生模块
换过 Node 大版本之后安装就废了,而且安装脚本还会报告成功。原生模块只能在
编译它的那个 Node ABI 上加载(Node 20 = 115、22 = 127、24 = 137),而
better-sqlite3 的 .node 放在与 ABI 无关的固定路径下,旧的 download-binaries
只检查「文件存在且大于 500KB」,于是给 Node 24 编译的 1.9MB 文件在 Node 22
下原样保留,跳过重新下载,机器人启动时死在 NODE_MODULE_VERSION 上。
(@discordjs/opus 的目录名里带 ABI,反而歪打正着没这个问题。)

download-binaries.mjs 现在不看文件大小,而是在子进程里真的把每个包 load 一遍
——子进程是必须的,Windows 上父进程加载过的 .node 会一直被映射,系统随后拒绝
删除或覆盖它。注意 better-sqlite3 的 addon 是在 Database 构造函数里惰性加载的,
所以光 require 这个包探测不出问题,得真的开一个内存库。

失败就按当前 ABI 重新安装,整个替换过程是先把旧文件挪到 node_modules/
.tsmusicbot-backup、下载解压到暂存目录、原子 rename 就位、再探测一次,任何
一步失败都把原文件还原回去——删掉不匹配的二进制却下载不下来,比原来的版本
更糟。备份特意放在包的 build/ 之外,因为源码编译回退会调 node-gyp 把 build/
清空。被中断(比如下载到一半 Ctrl+C)遗留的备份,下一次运行会自动认领回来。

其他一并修掉的问题:
- 版本号原本硬编码 12.8.0,实际锁的是 12.11.1,一旦真的触发下载就会 404;
  改为从 node_modules 里读。
- 三个模块原本用 Promise.all 并发。源码编译走的是 execSync,会把事件循环整个
  卡住几分钟,而 download() 的 120 秒超时是挂在同一个循环上的 socket 静默计时
  器——循环一恢复,还在传输中的连接就会被判超时。这不是小概率竞态:npmmirror
  上没有 ABI 137 的 opus,也没有 ABI 115 的 better-sqlite3,所以在 Node 24 和
  Node 20 上必定有一个模块在 100ms 内 404 并开始编译,而 ffmpeg 的 80MB 下载
  正在进行。ffmpeg 是可选模块,于是它被误杀后只记一条 WARN,脚本照样 exit 0,
  setup 打印「Setup Complete」,用户装完却没有 ffmpeg,放什么都放不出来。
  改成严格串行执行。
- 必需模块(opus / better-sqlite3)失败才返回非零;ffmpeg 有系统 ffmpeg 兜底,
  只警告。setup.bat 里原本形同虚设的 FAILED 标志接上了,必需模块失败会中止安装,
  不再是「装完才发现」。
- 4b 步骤原本把全部输出重定向进 setup.log,用户盯着不动的窗口以为卡死;现在
  进度走 stderr 实时显示,完整记录仍进日志。

新增 scripts/check-native.mjs:启动前预检,直接说清楚哪个模块对不上、分别是哪
个 ABI、怎么修,而不是抛一串 NODE_MODULE_VERSION 堆栈。scripts\start.bat、根目
录 start.bat(现在改为委托给前者,并且会先切到项目目录)和 npm start 的 prestart
都会跑它。Docker 运行镜像也补上这个文件,否则容器里执行 npm start 会因为找不到
脚本而失败。

Node 版本要求改为按依赖的真实下限判断(@honeybbq/teamspeak-client 要 >=20.19、
@sansenjian/qq-music-api 要 >=20.17/22.9,21 和 23 被 better-sqlite3 与 vitest
排除),package.json 补上对应的 engines;比 20/22 LTS 更新的大版本不阻止,只提
示可能要源码编译。README 相应更新,并补一条 NODE_MODULE_VERSION 的常见问题。

注意:批处理里新增的行全部保持纯 ASCII —— cmd.exe 在括号块里遇到多字节 UTF-8
会算错文件偏移,开始吃掉后续行的 echo 前缀,中文提示一律交给 Node 脚本输出。

Closes #140

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 15:09:15 +08:00
itsericrao f637ba0191 Jellyfin Integration. Assisted by Claude Fable 5. 2026-07-06 23:07:20 +08:00
saopig1andClaude Opus 4.8 f6e42811a5 chore(deps): update NeteaseCloudMusicApi (pinned) + safe patch/minor bumps
Dependency audit follow-up to the QQ pin:
- NeteaseCloudMusicApi ^4.30.0 → ~4.32.0. Same rationale as @sansenjian/
  qq-music-api: it's an embedded API server loaded via dynamic import, so a loose
  `^` could drift into a breaking minor and silently kill the netease server
  (ECONNREFUSED). Tighten to ~4.32.x. Verified at runtime: server starts on 3001
  and /banner returns 200.
- Lockfile bumps within existing ^ ranges (no API-server risk, so ranges kept):
  better-sqlite3 12.8.0→12.11.1, koa 3.2.0→3.2.1, ws 8.20.0→8.21.0,
  typescript 6.0.2→6.0.3, ts3-nodejs-library 3.5.1→3.5.3, vitest 4.1.4→4.1.9,
  tsx 4.21.0→4.22.4.
- Deliberately NOT bumped (major / needs a migration): bcryptjs 2→3 (password
  hashing), @types/node 25→26, @types/supertest 6→7.

tsc clean; full suite 913 passing under vitest 4.1.9.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 23:16:25 +08:00
saopig1andClaude Opus 4.8 08260182a9 fix(qq): pin @sansenjian/qq-music-api to ~2.4.0 + guard startup failures
A loose `^2.2.10` range let npm pull a newer build of the QQ Music API. The
library went ESM in 2.3.x: an ESM-only 2.3.0/2.3.1 throws ERR_REQUIRE_ESM on
load, so the embedded server never binds port 3200 and every QQ request
(including /getQQLoginQr) fails downstream with ECONNREFUSED — the QR code never
appears and cookies look broken.

- Pin to `~2.4.0` (verified: loads via the bot's native ESM import, exposes the
  Koa app, and every endpoint qq.ts calls returns the exact shapes it parses —
  QR, recommend, lyric, play, playlist detail). Blocks the broken 2.3.0/2.3.1
  and any future 2.5 migration. NOTE: 2.4.x requires Node >=20.17 (or >=22.9).
- Add describeQqApiStartupError(): on startup failure, log an actionable error
  (ERR_REQUIRE_ESM → version-pin hint; engine mismatch → Node-upgrade hint)
  instead of a generic warning, so this is obvious from the logs next time.
- README: troubleshooting entry for "QQ 二维码不弹 / 登录失败 / cookie 无法使用"
  and the version/Node note in the dependency table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 23:05:47 +08:00
saopig1 df79976933 deps: add bcryptjs + cookie-parser + supertest for WebUI auth 2026-05-27 13:25:32 +08:00
Claude 47e0d0f288 fix: resolve Docker FFmpeg SIGSEGV crash and build failures (#24)
The ffmpeg-static npm package bundles a pre-compiled binary that passes
`ffmpeg -version` but crashes with SIGSEGV during actual audio processing
inside Docker containers (incompatible glibc/missing shared libraries).

Changes:
- Install system FFmpeg via apt-get in Docker production stage, which is
  always compatible with the container runtime
- Reverse FFmpeg resolution priority in player.ts: prefer system FFmpeg,
  fall back to ffmpeg-static (for non-Docker environments like Windows)
- Optimize Dockerfile multi-stage build: compile native modules (opus,
  better-sqlite3) in builder stage and copy to production, eliminating
  the need for build tools (python3, make, g++) in the production image
- Fix qq-music-api dependency from file:../qq-music-api (path outside
  Docker build context, breaks npm ci) to npm registry ^2.2.10

https://claude.ai/code/session_01JAV8sBokoifKh4Hc8XbJws
2026-04-12 14:41:48 +00:00
saopig1andClaude Opus 4.6 5647cf6d36 feat(qq): consume local @sansenjian/qq-music-api fork with VIP-aware getMusicPlay
Repoints the `@sansenjian/qq-music-api` dependency from the public npm
release to a local fork at ../qq-music-api, which ships a corrected
getMusicPlay that:
  - drops the hardcoded-sign GET path (no longer honored by QQ's vkey
    server for VIP entitlement lookups)
  - POSTs JSON directly to u.y.qq.com/cgi-bin/musicu.fcg (mirroring
    the library's own getLyric.ts pattern)
  - extracts qqmusic_key from the forwarded cookie and passes it as
    `comm.authst` — the inline auth field the jsososo/QQMusicApi
    reference implementation sets
  - uses `ct: 19` (was 24) to match the community reference

For accounts that actually have entitlement to a given track, this
now returns the real VIP URL. For accounts that don't, QQ's vkey
server still returns result=104003 with empty purl — this is correct
server-side behavior and not a bug. Verified by observing the real
QQ Music web player on y.qq.com fall back to the same 30-second
preview on a logged-in account that lacks the specific track tier.

Supporting changes:

  src/music/api-server.ts
    The fork (v2.2.11) stopped auto-starting a Koa server on import —
    it only listens when run as `require.main`. Explicitly import the
    default Koa app and call .listen() with a server handle we can
    clean up on shutdown. Without this fix, port 3200 silently fails
    to bind and every QQ endpoint 502s.

  src/music/qq.ts (getSongDetail)
    The library's /getSongInfo endpoint returns upstream code 500001
    because its param format no longer matches QQ's current API.
    resolveAndPlay only needs `id` + `platform` to fetch a play URL,
    so fall through to a minimal stub on /getSongInfo failure. This
    unblocks /play-by-id and /add-by-id for QQ — they had been
    returning "Song not found" for every QQ track regardless of
    entitlement.

  scripts/qq_browser_login.py
    Visible-browser diagnostic tool that opens Chromium at y.qq.com,
    auto-detects login via uin cookie poll, captures the full
    post-login cookie set, tests it against /getMusicPlay for 稻香,
    and writes the cookie to data/cookies/qq.json only if VIP
    actually unlocks. On failure, dumps the full cookie to
    data/cookies/qq.browser-capture.json for OAuth-vs-browser diff.

  scripts/qq_verify_entitlement.py
    Companion diagnostic: opens the real QQ Music web player at a
    specific song's detail page so the user can manually click play
    and verify whether their account has entitlement — independent
    of any code path in this project. If the browser plays the full
    song, HTTP 104003 is a request-signing issue; if the browser
    also falls back to a 30-second preview, the account lacks the
    tier/album purchase and no code fix can change that.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 22:52:12 +08:00
saopig1andClaude Opus 4.6 ca4cb1790a chore(deps): bump @honeybbq/teamspeak-client to 0.2.1, drop ts6-compat shim
Version 0.2.1 ships a universal clientinit format that works natively
against both TS3 and TS6 servers:

    client_version: "3.?.? [Build: 5680278000]"
    client_version_sign: DX5NIYLvfJEUjuIbCidnoeozxIDRRkpq3I9vVMBmE9L2qnekOo
                         BzSenkzsg2lC9CMv8K5hkEzhr2TYUYSwUXCg==

The old ts6-compat.ts workaround (monkey-patching handler.sendPacket to
rewrite clientinit's client_version to "3.6.2") is now actively wrong:
it replaces the library's new correct version/signature pair with a
stale one that TS6 servers reject, which is why the first 0.2.1 TS6
handshake attempt still hung at `received initivexpand2`.

Changes:
- package.json: "@honeybbq/teamspeak-client": "^0.1.0" -> "^0.2.1"
- src/ts-protocol/client.ts: remove patchClientInitVersion import and
  the sendPacket monkey-patch block. Leave an inline comment so anyone
  reading the git blame understands why the shim is gone.
- Delete src/ts-protocol/ts6-compat.ts and ts6-compat.test.ts — no
  callers remain.

Other 0.2.x notes worth knowing (no code change here, just documenting):
- ClientOptions gained serverPassword / defaultChannel /
  defaultChannelPassword that are sent DURING clientinit. We still call
  our own joinChannel() post-connect because the existing flow works
  and switching is an orthogonal refactor.
- 0.1.1 contains the P-256 DER encoding fix (PR #5 by ZHANGTIANYAO1).
  Identities generated by 0.1.0 are cryptographically incompatible with
  0.2.x's corrected handshake path — a bot whose identity column was
  populated before this upgrade will hang at `received initivexpand2`
  and fall through the 15s connect deadline. Workaround: clear the
  identity column so the next start generates a fresh key. Server
  groups assigned to the old UID must be re-granted once against the
  new one.

Verification:
- tsc --noEmit clean
- vitest: 93/93 unit tests pass (1 test file removed with ts6-compat)
- scripts/test_full_feature.py against a local TS6 server: 51/51 pass,
  including handshake, voice playback, identity persistence, WebSocket
  stateChange broadcasts, and all corner-case regressions.
- Live: bot connected to TS6 in ~80ms after identity regeneration,
  played NetEase audio through the voice channel, clean stop.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 01:54:43 +08:00
saopig1andClaude Sonnet 4.6 d5d6abeb1e feat: implement server password login, YouTube source, and improved bot selector UI
- **TS server password** (#7/#9): add serverPassword field across database,
  manager, bot API, and Settings UI — allows joining password-protected servers
- **YouTube audio source** (#1/#10): new YouTubeProvider using yt-dlp binary;
  adds -y flag in chat commands, /api/music supports platform=youtube,
  YouTube badge in SongCard, yt-dlp-wrap npm dependency
- **Bot selector UI** (#14): selector always visible (not just when >1 bot),
  bigger button with border and play-state indicator; per-bot URL routing at
  /bot/:id with BotRedirect view; copy-link button in dropdown

Closes #1, #7, #9, #10, #14

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 11:58:07 +08:00
Claude d5a7351be7 Add missing "play" script to package.json
Adds a "play" script as an alias for "start" (both run `node dist/index.js`).
This resolves confusion where users might try `npm run play` based on the
bot's music-playing nature.

Fixes #11

https://claude.ai/code/session_01GC6qsKsmiroNkruLENDhPq
2026-04-07 12:44:18 +00:00
saopig1andClaude Opus 4.6 61d9bbe0b7 feat: bundle FFmpeg via ffmpeg-static for one-click deployment
Users no longer need to install FFmpeg separately — it is now bundled as
an npm dependency (ffmpeg-static). player.ts resolves the bundled binary
automatically and falls back to system PATH. start.bat and a new
setup.bat script handle dependency installation and project building.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 03:43:10 +08:00
saopig1andClaude Opus 4.6 0559f09f0a feat: replace ServerQuery with full TS3 client protocol — bot is now VISIBLE
Switch from TCP ServerQuery (port 10011, invisible) to real TS3 client
protocol via @honeybbq/teamspeak-client (UDP 9987, ECDH handshake,
AES-EAX encryption). Bot now appears as a regular client visible to
all users in TeamSpeak.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 01:28:25 +08:00
saopig1andClaude Opus 4.6 694e3c953b feat: add Vue.js WebUI — YesPlayMusic-inspired SPA with all pages and components
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 00:58:33 +08:00
saopig1andClaude Opus 4.6 94902fc2c4 feat: add music source service — NetEase/QQ providers, unified interface, cookie auth
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 00:39:47 +08:00
saopig1andClaude Opus 4.6 5afc8c5224 feat: add audio engine — Opus encoder, play queue with 4 modes, FFmpeg player
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 00:34:57 +08:00
saopig1andClaude Opus 4.6 2b6e81119b feat: add TS3 identity generation with Ed25519 keypairs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 00:28:12 +08:00
saopig1andClaude Opus 4.6 101d593c0e chore: initialize project with TypeScript and dependencies
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 00:16:01 +08:00