From c86d70ca08d0be4b8d51bf3b60e0d703a801efa6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 14:31:49 +0000 Subject: [PATCH 01/11] fix: correct git clone URL in README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repo URL was pointing to the old `tsmusicbot.git` instead of the actual repository name `teamspeak-music-bot.git`. Fixed both occurrences (方式二 and 方式三 sections). https://claude.ai/code/session_01VTscSE9PxD6qwF56WBMunQ --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index dc17d10..a950a9b 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,8 @@ FFmpeg **已自动内置**,无需手动安装。 ```bash # 下载项目 -git clone https://github.com/ZHANGTIANYAO1/tsmusicbot.git -cd tsmusicbot +git clone https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git +cd teamspeak-music-bot # 安装依赖 npm install @@ -86,8 +86,8 @@ npm start 所有依赖已内置(Node.js、FFmpeg、Opus 编码器),无需安装任何额外软件。 ```bash -git clone https://github.com/ZHANGTIANYAO1/tsmusicbot.git -cd tsmusicbot/scripts/docker +git clone https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git +cd teamspeak-music-bot/scripts/docker docker-compose up -d ``` From c950c7402fb52367bc91532e8351fb1f456f9fcc Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 14:32:34 +0000 Subject: [PATCH 02/11] fix: correct project root directory name in README architecture section Changed `tsmusicbot/` to `teamspeak-music-bot/` in the project architecture directory tree to match the actual repository name. https://claude.ai/code/session_01VTscSE9PxD6qwF56WBMunQ --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a950a9b..b20a224 100644 --- a/README.md +++ b/README.md @@ -279,7 +279,7 @@ sudo systemctl start tsmusicbot ## 项目架构 ``` -tsmusicbot/ +teamspeak-music-bot/ ├── src/ # 后端源码 (TypeScript) │ ├── audio/ # 音频管线:FFmpeg → PCM → Opus → 20ms 帧 │ │ ├── encoder.ts # Opus 编码器 (@discordjs/opus) From 47e0d0f288a5ba740792b699f7737aa694f2d63b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 14:41:48 +0000 Subject: [PATCH 03/11] 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 --- package-lock.json | 989 +++++++++++++++++++----------- package.json | 2 +- scripts/docker/Dockerfile | 23 +- scripts/docker/docker-compose.yml | 4 +- src/audio/player.ts | 25 +- 5 files changed, 649 insertions(+), 394 deletions(-) diff --git a/package-lock.json b/package-lock.json index dc0b5f7..2dbf969 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@discordjs/opus": "^0.10.0", "@honeybbq/teamspeak-client": "^0.2.1", "@koa/router": "^15.4.0", - "@sansenjian/qq-music-api": "file:../qq-music-api", + "@sansenjian/qq-music-api": "^2.2.10", "axios": "^1.14.0", "better-sqlite3": "^12.8.0", "chalk": "^5.6.2", @@ -38,62 +38,6 @@ "vitest": "^4.1.2" } }, - "../qq-music-api": { - "name": "@sansenjian/qq-music-api", - "version": "2.2.11", - "license": "MIT", - "dependencies": { - "@koa/router": "^15.3.1", - "axios": "^1.13.6", - "date-fns": "^4.1.0", - "is-generator-function": "1.0.10", - "koa": "^2.16.1", - "koa-bodyparser": "^4.4.1", - "koa-static": "^5.0.0", - "reflect-metadata": "^0.2.2" - }, - "bin": { - "qq-music-api": "dist/app.js" - }, - "devDependencies": { - "@commitlint/cli": "^18.0.0", - "@commitlint/config-conventional": "^18.0.0", - "@types/jest": "^30.0.0", - "@types/koa": "^3.0.1", - "@types/koa__router": "^12.0.5", - "@types/koa-bodyparser": "^4.3.13", - "@types/koa-static": "^4.0.4", - "@types/node": "^25.3.3", - "@types/supertest": "^7.2.0", - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", - "chalk": "^4.1.0", - "conventional-changelog-cli": "^4.0.0", - "eslint": "^8.57.0", - "eslint-config-standard": "^17.0.0", - "eslint-config-standard-with-typescript": "^43.0.1", - "eslint-plugin-import": "^2.29.0", - "eslint-plugin-n": "^16.0.0", - "eslint-plugin-promise": "^6.0.0", - "husky": "^9.0.0", - "jest": "^30.2.0", - "lint-staged": "^15.0.0", - "nodemon": "^3.1.14", - "prettier": "^3.8.1", - "rimraf": "^6.1.3", - "sinon": "^21.0.1", - "supertest": "^7.2.2", - "ts-jest": "^29.4.6", - "ts-node": "^10.9.2", - "tsx": "^4.21.0", - "typescript": "5.7.3", - "vitepress": "^1.6.4", - "vue": "^3.5.29" - }, - "engines": { - "node": "22.x" - } - }, "node_modules/@derhuerst/http-basic": { "version": "8.2.4", "resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz", @@ -144,46 +88,43 @@ } }, "node_modules/@emnapi/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", - "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.0", + "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", - "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", - "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "cpu": [ "ppc64" ], @@ -198,9 +139,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "cpu": [ "arm" ], @@ -215,9 +156,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "cpu": [ "arm64" ], @@ -232,9 +173,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "cpu": [ "x64" ], @@ -249,9 +190,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], @@ -266,9 +207,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "cpu": [ "x64" ], @@ -283,9 +224,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "cpu": [ "arm64" ], @@ -300,9 +241,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "cpu": [ "x64" ], @@ -317,9 +258,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "cpu": [ "arm" ], @@ -334,9 +275,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "cpu": [ "arm64" ], @@ -351,9 +292,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "cpu": [ "ia32" ], @@ -368,9 +309,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "cpu": [ "loong64" ], @@ -385,9 +326,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "cpu": [ "mips64el" ], @@ -402,9 +343,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "cpu": [ "ppc64" ], @@ -419,9 +360,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "cpu": [ "riscv64" ], @@ -436,9 +377,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "cpu": [ "s390x" ], @@ -453,9 +394,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], @@ -470,9 +411,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", "cpu": [ "arm64" ], @@ -487,9 +428,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "cpu": [ "x64" ], @@ -504,9 +445,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "cpu": [ "arm64" ], @@ -521,9 +462,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "cpu": [ "x64" ], @@ -538,9 +479,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", "cpu": [ "arm64" ], @@ -555,9 +496,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "cpu": [ "x64" ], @@ -572,9 +513,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "cpu": [ "arm64" ], @@ -589,9 +530,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "cpu": [ "ia32" ], @@ -606,9 +547,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "cpu": [ "x64" ], @@ -629,9 +570,9 @@ "license": "BSD-3-Clause" }, "node_modules/@honeybbq/teamspeak-client": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@honeybbq/teamspeak-client/-/teamspeak-client-0.2.1.tgz", - "integrity": "sha512-iLZRWsqeAA/IuEL0zMAhHm9+wQ1YmByXjh9T6PZLYSxFk56T39MpQobT6eohM/PnIz3wQZymd8h701hyliLo2w==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@honeybbq/teamspeak-client/-/teamspeak-client-0.2.2.tgz", + "integrity": "sha512-JQ4yHEUwAxK2kHmIxelEf3grzMM8lVeuf7RzxK1g24U6XH8ZJO28S477FUtSTRe68PwUBkAlsa1Z/7BhLKh+AA==", "license": "MIT", "dependencies": { "@noble/curves": "^2.0.1" @@ -671,9 +612,9 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", - "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", + "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", "dev": true, "license": "MIT", "optional": true, @@ -717,9 +658,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.122.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", - "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", + "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", "dev": true, "license": "MIT", "funding": { @@ -733,9 +674,9 @@ "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", "cpu": [ "arm64" ], @@ -750,9 +691,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", "cpu": [ "arm64" ], @@ -767,9 +708,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", "cpu": [ "x64" ], @@ -784,9 +725,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", "cpu": [ "x64" ], @@ -801,9 +742,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", - "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", + "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", "cpu": [ "arm" ], @@ -818,9 +759,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", "cpu": [ "arm64" ], @@ -835,9 +776,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", "cpu": [ "arm64" ], @@ -852,9 +793,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", "cpu": [ "ppc64" ], @@ -869,9 +810,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", "cpu": [ "s390x" ], @@ -886,9 +827,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", "cpu": [ "x64" ], @@ -903,9 +844,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", "cpu": [ "x64" ], @@ -920,9 +861,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", "cpu": [ "arm64" ], @@ -937,9 +878,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", - "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", + "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", "cpu": [ "wasm32" ], @@ -947,16 +888,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.3" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", "cpu": [ "arm64" ], @@ -971,9 +914,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", "cpu": [ "x64" ], @@ -988,15 +931,196 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", - "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", + "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", "dev": true, "license": "MIT" }, "node_modules/@sansenjian/qq-music-api": { - "resolved": "../qq-music-api", - "link": true + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/@sansenjian/qq-music-api/-/qq-music-api-2.2.10.tgz", + "integrity": "sha512-EjhZ3s+ZErSrO6jt5BkThIq6hmMFfvr6RNg945m8SrBOHfuih0cC2kbpoz7WgwnOXoq/psN8iVG/jXrYRUFyQA==", + "license": "MIT", + "dependencies": { + "@koa/router": "^15.3.1", + "axios": "^1.13.6", + "date-fns": "^4.1.0", + "is-generator-function": "1.0.10", + "koa": "^2.16.1", + "koa-bodyparser": "^4.4.1", + "koa-static": "^5.0.0", + "reflect-metadata": "^0.2.2" + }, + "bin": { + "qq-music-api": "dist/app.js" + }, + "engines": { + "node": "22.x" + } + }, + "node_modules/@sansenjian/qq-music-api/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sansenjian/qq-music-api/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sansenjian/qq-music-api/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sansenjian/qq-music-api/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sansenjian/qq-music-api/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sansenjian/qq-music-api/node_modules/http-errors/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sansenjian/qq-music-api/node_modules/koa": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.4.tgz", + "integrity": "sha512-3An0GCLDSR34tsCO4H8Tef8Pp2ngtaZDAZnsWJYelqXUK5wyiHvGItgK/xcSkmHLSTn1Jcho1mRQs2ehRzvKKw==", + "license": "MIT", + "dependencies": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.9.0", + "debug": "^4.3.2", + "delegates": "^1.0.0", + "depd": "^2.0.0", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^2.0.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + }, + "engines": { + "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4" + } + }, + "node_modules/@sansenjian/qq-music-api/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sansenjian/qq-music-api/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sansenjian/qq-music-api/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sansenjian/qq-music-api/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sansenjian/qq-music-api/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sansenjian/qq-music-api/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } }, "node_modules/@standard-schema/spec": { "version": "1.1.0", @@ -1117,13 +1241,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", - "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": "~7.19.0" } }, "node_modules/@types/qs": { @@ -1172,16 +1296,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", - "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz", + "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.2", - "@vitest/utils": "4.1.2", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -1190,13 +1314,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", - "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz", + "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.2", + "@vitest/spy": "4.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1217,9 +1341,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", - "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz", + "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==", "dev": true, "license": "MIT", "dependencies": { @@ -1230,13 +1354,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", - "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz", + "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.2", + "@vitest/utils": "4.1.4", "pathe": "^2.0.3" }, "funding": { @@ -1244,14 +1368,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", - "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz", + "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.2", - "@vitest/utils": "4.1.2", + "@vitest/pretty-format": "4.1.4", + "@vitest/utils": "4.1.4", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1260,9 +1384,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", - "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz", + "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==", "dev": true, "license": "MIT", "funding": { @@ -1270,13 +1394,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", - "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz", + "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.2", + "@vitest/pretty-format": "4.1.4", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -1424,9 +1548,9 @@ } }, "node_modules/axios": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", - "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", @@ -1461,9 +1585,9 @@ "license": "MIT" }, "node_modules/basic-ftp": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", - "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.2.tgz", + "integrity": "sha512-1tDrzKsdCg70WGvbFss/ulVAxupNauGnOlgpyjKzeQxzyllBLS0CGLV7tjIXTK3ZQA9/FBEm9qyFFN1bciA6pw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -1543,9 +1667,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -1620,6 +1744,40 @@ "node": ">= 0.8" } }, + "node_modules/cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "license": "MIT", + "dependencies": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/cache-content-type/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cache-content-type/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1696,10 +1854,13 @@ } }, "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } }, "node_modules/cliui": { "version": "8.0.1", @@ -1715,6 +1876,16 @@ "node": ">=12" } }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, "node_modules/co-body": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.2.0.tgz", @@ -1868,9 +2039,9 @@ "license": "ISC" }, "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "license": "MIT", "engines": { "node": ">=18" @@ -1971,6 +2142,16 @@ "node": ">= 14" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2196,9 +2377,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2209,32 +2390,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.4", - "@esbuild/android-arm": "0.27.4", - "@esbuild/android-arm64": "0.27.4", - "@esbuild/android-x64": "0.27.4", - "@esbuild/darwin-arm64": "0.27.4", - "@esbuild/darwin-x64": "0.27.4", - "@esbuild/freebsd-arm64": "0.27.4", - "@esbuild/freebsd-x64": "0.27.4", - "@esbuild/linux-arm": "0.27.4", - "@esbuild/linux-arm64": "0.27.4", - "@esbuild/linux-ia32": "0.27.4", - "@esbuild/linux-loong64": "0.27.4", - "@esbuild/linux-mips64el": "0.27.4", - "@esbuild/linux-ppc64": "0.27.4", - "@esbuild/linux-riscv64": "0.27.4", - "@esbuild/linux-s390x": "0.27.4", - "@esbuild/linux-x64": "0.27.4", - "@esbuild/netbsd-arm64": "0.27.4", - "@esbuild/netbsd-x64": "0.27.4", - "@esbuild/openbsd-arm64": "0.27.4", - "@esbuild/openbsd-x64": "0.27.4", - "@esbuild/openharmony-arm64": "0.27.4", - "@esbuild/sunos-x64": "0.27.4", - "@esbuild/win32-arm64": "0.27.4", - "@esbuild/win32-ia32": "0.27.4", - "@esbuild/win32-x64": "0.27.4" + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" } }, "node_modules/escalade": { @@ -3037,6 +3218,21 @@ "node": ">=8" } }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -3147,6 +3343,19 @@ "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", "license": "MIT" }, + "node_modules/koa-convert": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", + "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", + "license": "MIT", + "dependencies": { + "co": "^4.6.0", + "koa-compose": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/koa-send": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz", @@ -3230,6 +3439,31 @@ "node": ">= 0.6" } }, + "node_modules/koa/node_modules/accepts/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/koa/node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -3248,18 +3482,6 @@ "node": ">= 0.6" } }, - "node_modules/koa/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/koa/node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -4136,9 +4358,9 @@ } }, "node_modules/netmask": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", "license": "MIT", "engines": { "node": ">= 0.4.0" @@ -4284,6 +4506,11 @@ "wrappy": "1" } }, + "node_modules/only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==" + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -4407,9 +4634,9 @@ } }, "node_modules/path-to-regexp": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", - "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", "funding": { "type": "opencollective", @@ -4503,9 +4730,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", "dev": true, "funding": [ { @@ -4708,9 +4935,9 @@ } }, "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -4846,6 +5073,12 @@ "node": ">= 12.13.0" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4946,14 +5179,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", - "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", + "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.122.0", - "@rolldown/pluginutils": "1.0.0-rc.12" + "@oxc-project/types": "=0.124.0", + "@rolldown/pluginutils": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" @@ -4962,21 +5195,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-x64": "1.0.0-rc.12", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + "@rolldown/binding-android-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-x64": "1.0.0-rc.15", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" } }, "node_modules/router": { @@ -5134,13 +5367,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -5468,6 +5701,12 @@ "tar-stream": "^2.1.4" } }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", @@ -5484,15 +5723,6 @@ "node": ">=6" } }, - "node_modules/tar/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/thread-stream": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz", @@ -5513,9 +5743,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", - "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", "dev": true, "license": "MIT", "engines": { @@ -5523,14 +5753,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -5688,9 +5918,9 @@ } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "dev": true, "license": "MIT" }, @@ -5728,16 +5958,16 @@ } }, "node_modules/vite": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", - "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", + "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.12", + "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "bin": { @@ -5755,7 +5985,7 @@ "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", - "esbuild": "^0.27.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", @@ -5806,19 +6036,19 @@ } }, "node_modules/vitest": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", - "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz", + "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.2", - "@vitest/mocker": "4.1.2", - "@vitest/pretty-format": "4.1.2", - "@vitest/runner": "4.1.2", - "@vitest/snapshot": "4.1.2", - "@vitest/spy": "4.1.2", - "@vitest/utils": "4.1.2", + "@vitest/expect": "4.1.4", + "@vitest/mocker": "4.1.4", + "@vitest/pretty-format": "4.1.4", + "@vitest/runner": "4.1.4", + "@vitest/snapshot": "4.1.4", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -5846,10 +6076,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.2", - "@vitest/browser-preview": "4.1.2", - "@vitest/browser-webdriverio": "4.1.2", - "@vitest/ui": "4.1.2", + "@vitest/browser-playwright": "4.1.4", + "@vitest/browser-preview": "4.1.4", + "@vitest/browser-webdriverio": "4.1.4", + "@vitest/coverage-istanbul": "4.1.4", + "@vitest/coverage-v8": "4.1.4", + "@vitest/ui": "4.1.4", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -5873,6 +6105,12 @@ "@vitest/browser-webdriverio": { "optional": true }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, "@vitest/ui": { "optional": true }, @@ -6043,6 +6281,15 @@ "node": ">=12" } }, + "node_modules/ylru": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.4.0.tgz", + "integrity": "sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/yt-dlp-wrap": { "version": "2.3.12", "resolved": "https://registry.npmjs.org/yt-dlp-wrap/-/yt-dlp-wrap-2.3.12.tgz", diff --git a/package.json b/package.json index c027b42..e13e740 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@discordjs/opus": "^0.10.0", "@honeybbq/teamspeak-client": "^0.2.1", "@koa/router": "^15.4.0", - "@sansenjian/qq-music-api": "file:../qq-music-api", + "@sansenjian/qq-music-api": "^2.2.10", "axios": "^1.14.0", "better-sqlite3": "^12.8.0", "chalk": "^5.6.2", diff --git a/scripts/docker/Dockerfile b/scripts/docker/Dockerfile index 67a2691..eee9a67 100644 --- a/scripts/docker/Dockerfile +++ b/scripts/docker/Dockerfile @@ -25,30 +25,27 @@ RUN cd web && npm ci COPY . . RUN npm run build +# Install production dependencies (with native addons compiled) in the builder +# so we don't need build tools in the production image. +RUN rm -rf node_modules && npm ci --production && npm cache clean --force + # --- Stage 2: Production image --- FROM node:20-slim -# Install only runtime native build tools (needed for native module rebuild) +# Install system FFmpeg — the ffmpeg-static npm package bundles a pre-compiled +# binary that can SIGSEGV inside Docker (incompatible glibc / missing libs). +# System-installed FFmpeg is always compatible with the container runtime. RUN apt-get update && apt-get install -y --no-install-recommends \ - python3 make g++ && \ + ffmpeg && \ rm -rf /var/lib/apt/lists/* WORKDIR /app -# Copy built output +# Copy built output and pre-compiled production node_modules from builder COPY --from=builder /app/dist ./dist COPY --from=builder /app/web/dist ./web/dist COPY --from=builder /app/package*.json ./ - -# Install production dependencies (includes ffmpeg-static, opus, sqlite3) -# These need to compile native addons inside the container -RUN npm ci --production && npm cache clean --force - -# Remove build tools to reduce image size -RUN apt-get purge -y python3 make g++ && apt-get autoremove -y && \ - rm -rf /var/lib/apt/lists/* - -# FFmpeg is bundled via ffmpeg-static — no system ffmpeg needed +COPY --from=builder /app/node_modules ./node_modules # Data directory for database, cookies, logs RUN mkdir -p /app/data diff --git a/scripts/docker/docker-compose.yml b/scripts/docker/docker-compose.yml index 0550319..f836195 100644 --- a/scripts/docker/docker-compose.yml +++ b/scripts/docker/docker-compose.yml @@ -1,8 +1,8 @@ # TSMusicBot — Docker Compose # 一键部署:docker-compose up -d # -# 所有依赖已内置(Node.js, FFmpeg, Opus 编码器) -# 无需安装任何额外软件 +# 所有依赖已内置(Node.js, FFmpeg, Opus 编码器)无需安装任何额外软件 +# FFmpeg 使用系统包管理器安装,确保容器内兼容性 services: tsmusicbot: diff --git a/src/audio/player.ts b/src/audio/player.ts index 6a0f970..66fcefe 100644 --- a/src/audio/player.ts +++ b/src/audio/player.ts @@ -35,28 +35,39 @@ function ffmpegWorks(bin: string): boolean { } } -/** Resolved once at module load — prefer bundled ffmpeg-static, fall back to system. */ +/** + * Resolved once at module load. + * + * Priority: system FFmpeg → bundled ffmpeg-static. + * + * System-installed FFmpeg is always compatible with the running OS/container, + * while the pre-compiled binary from ffmpeg-static can SIGSEGV in Docker + * (passes `ffmpeg -version` but crashes during actual audio processing due to + * incompatible glibc or missing shared libraries). + */ const resolvedFfmpeg: string = (() => { + // 1. Prefer system-installed FFmpeg (always compatible with the runtime) + if (ffmpegWorks("ffmpeg")) { + return "ffmpeg"; + } + + // 2. Fall back to bundled ffmpeg-static binary // On Windows, ffmpeg-static may return a path with backslashes; on Linux/macOS // it may return a Windows .exe path if node_modules was copied cross-platform. const isWinPath = ffmpegPath ? /\\/.test(ffmpegPath) || ffmpegPath.endsWith(".exe") : false; const onWindows = process.platform === "win32"; - // Only try bundled binary if platform matches if (ffmpegPath && (onWindows === isWinPath)) { if (isExecutable(ffmpegPath) && ffmpegWorks(ffmpegPath)) { return ffmpegPath; } } - // Fall back to system ffmpeg - if (ffmpegWorks("ffmpeg")) { - return "ffmpeg"; - } + // Last resort: always use "ffmpeg" so spawn error is clear, never use a cross-platform path return "ffmpeg"; })(); -/** Resolve ffmpeg binary: prefer bundled ffmpeg-static, fall back to system PATH. */ +/** Resolve ffmpeg binary: prefer system PATH, fall back to bundled ffmpeg-static. */ function getFfmpegCommand(): string { return resolvedFfmpeg; } From 4eac2e4dde1f0fd1aa8469d3e3e30836dbb3fff1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 14:53:15 +0000 Subject: [PATCH 04/11] fix: Random mode now stops after all songs played instead of looping forever MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In Random (shuffle) mode, the queue's next() method would return the same song indefinitely when only one song was in the playlist, and never terminate even with multiple songs. This happened because played songs were not tracked. Added a playedIndices Set to track which songs have already been played in Random mode. Once all songs have been played once, next() returns null to stop playback — matching the expected behavior where Random plays each song once in random order, while RandomLoop is the mode for infinite shuffling. https://claude.ai/code/session_01W3ZncxL5VfdZeB4qqYWDmY --- src/audio/queue.test.ts | 26 ++++++++++++++++++++++++++ src/audio/queue.ts | 35 +++++++++++++++++++++++------------ 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/audio/queue.test.ts b/src/audio/queue.test.ts index e801014..e956d6e 100644 --- a/src/audio/queue.test.ts +++ b/src/audio/queue.test.ts @@ -150,6 +150,32 @@ describe("PlayQueue", () => { expect(next).not.toBeNull(); }); + it("random mode with single song returns null on next", () => { + queue.setMode(PlayMode.Random); + queue.add(makeSong("1")); + queue.play(); + expect(queue.next()).toBeNull(); + }); + + it("random mode plays each song exactly once then stops", () => { + queue.setMode(PlayMode.Random); + queue.add(makeSong("A")); + queue.add(makeSong("B")); + queue.add(makeSong("C")); + queue.play(); + const played = new Set(); + played.add(queue.current()!.id); + for (let i = 0; i < 3; i++) { + const song = queue.next(); + if (!song) break; + played.add(song.id); + } + // All 3 songs should have been played + expect(played).toEqual(new Set(["A", "B", "C"])); + // next() after all played should return null + expect(queue.next()).toBeNull(); + }); + it("random-loop mode never returns null", () => { queue.setMode(PlayMode.RandomLoop); queue.add(makeSong("1")); diff --git a/src/audio/queue.ts b/src/audio/queue.ts index 9c26ab0..7a6f36f 100644 --- a/src/audio/queue.ts +++ b/src/audio/queue.ts @@ -20,6 +20,7 @@ export class PlayQueue { private songs: QueuedSong[] = []; private currentIndex = -1; private mode: PlayMode = PlayMode.Sequential; + private playedIndices = new Set(); add(song: QueuedSong): void { this.songs.push(song); @@ -36,32 +37,36 @@ export class PlayQueue { if (index < this.currentIndex) { this.currentIndex--; } else if (index === this.currentIndex) { - // Move the pointer back by one so next() in sequential mode advances - // to the song that shifted into the removed slot. Without this, the - // shifted song is silently skipped because current() incorrectly - // returns it (even though the player is still on the removed song) - // and next() then increments past it. currentIndex may become -1, - // which is fine — it represents "no current song" and next() will - // pick index 0. this.currentIndex--; } + // Rebuild playedIndices to account for shifted indices + const newPlayed = new Set(); + for (const idx of this.playedIndices) { + if (idx === index) continue; + newPlayed.add(idx > index ? idx - 1 : idx); + } + this.playedIndices = newPlayed; + return removed; } clear(): void { this.songs = []; this.currentIndex = -1; + this.playedIndices.clear(); } play(): QueuedSong | null { if (this.songs.length === 0) return null; + this.playedIndices.clear(); this.currentIndex = 0; return this.songs[0]; } playAt(index: number): QueuedSong | null { if (index < 0 || index >= this.songs.length) return null; + this.playedIndices.clear(); this.currentIndex = index; return this.songs[index]; } @@ -81,11 +86,16 @@ export class PlayQueue { return this.songs[this.currentIndex]; } case PlayMode.Random: { - if (this.songs.length === 1) return this.songs[0]; - let nextIndex: number; - do { - nextIndex = Math.floor(Math.random() * this.songs.length); - } while (nextIndex === this.currentIndex && this.songs.length > 1); + if (this.currentIndex >= 0) { + this.playedIndices.add(this.currentIndex); + } + const unplayed: number[] = []; + for (let i = 0; i < this.songs.length; i++) { + if (!this.playedIndices.has(i)) unplayed.push(i); + } + if (unplayed.length === 0) return null; + const nextIndex = + unplayed[Math.floor(Math.random() * unplayed.length)]; this.currentIndex = nextIndex; return this.songs[nextIndex]; } @@ -141,6 +151,7 @@ export class PlayQueue { setMode(mode: PlayMode): void { this.mode = mode; + this.playedIndices.clear(); } getCurrentIndex(): number { From e2b1a5e055ef0199b56c9d43956b129d56574607 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 15:02:53 +0000 Subject: [PATCH 05/11] fix: prevent skipped/duplicate songs in Random mode edge cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three corner cases fixed: 1. Removing the currently-playing song caused the next song in the array to be silently marked as "played" and skipped. Root cause: playedIndices was updated in next() by marking currentIndex, but after remove() shifts currentIndex, it pointed to the wrong song. Fix: mark songs as played at play-time (in play/playAt/next/prev) instead of at next-request-time. 2. Using prev() in Random mode could cause a song to play twice — the song navigated to via prev() was not recorded in playedIndices, so next() could randomly select it again. Fix: prev() now marks the returned song as played. 3. Switching to Random mode mid-playback could cause the current song to repeat because setMode() cleared playedIndices without preserving the currently-playing song. Fix: setMode() now re-adds currentIndex after clearing. https://claude.ai/code/session_01W3ZncxL5VfdZeB4qqYWDmY --- src/audio/queue.test.ts | 69 +++++++++++++++++++++++++++++++++++++++++ src/audio/queue.ts | 10 ++++-- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/src/audio/queue.test.ts b/src/audio/queue.test.ts index e956d6e..dc58ce5 100644 --- a/src/audio/queue.test.ts +++ b/src/audio/queue.test.ts @@ -176,6 +176,75 @@ describe("PlayQueue", () => { expect(queue.next()).toBeNull(); }); + it("random mode: removing currently-playing song does not skip others", () => { + queue.setMode(PlayMode.Random); + queue.add(makeSong("A")); + queue.add(makeSong("B")); + queue.add(makeSong("C")); + queue.add(makeSong("D")); + queue.play(); // plays A (index 0) + const second = queue.next()!; // plays some song + // Remove the currently-playing song + const curIdx = queue.getCurrentIndex(); + queue.remove(curIdx); + // Remaining songs (excluding A and the removed song) should all be reachable + const played = new Set(); + played.add("A"); // already played via play() + played.add(second.id); // played and then removed + let song = queue.next(); + while (song) { + played.add(song.id); + song = queue.next(); + } + // All 4 original songs should have been played or accounted for + expect(played).toEqual(new Set(["A", "B", "C", "D"])); + }); + + it("random mode: prev does not cause duplicate plays", () => { + queue.setMode(PlayMode.Random); + queue.add(makeSong("A")); + queue.add(makeSong("B")); + queue.add(makeSong("C")); + queue.play(); // plays A + queue.next(); // plays B or C + queue.prev(); // go back — this song is now marked as played + // Exhaust remaining songs + const ids: string[] = []; + let song = queue.next(); + while (song) { + ids.push(song.id); + song = queue.next(); + } + // No song ID should appear more than once across the entire session + expect(new Set(ids).size).toBe(ids.length); + }); + + it("random mode: adding song mid-playback includes the new song", () => { + queue.setMode(PlayMode.Random); + queue.add(makeSong("A")); + queue.add(makeSong("B")); + queue.play(); // plays A + queue.next(); // plays B + // Add a new song while all existing songs have been played + queue.add(makeSong("C")); + const song = queue.next(); + expect(song).not.toBeNull(); + expect(song!.id).toBe("C"); + // After C, should stop + expect(queue.next()).toBeNull(); + }); + + it("random mode: setMode preserves current song as played", () => { + queue.add(makeSong("A")); + queue.add(makeSong("B")); + queue.play(); // plays A in sequential mode + queue.setMode(PlayMode.Random); // switch to random — A should be marked played + // next() should only return B, never A again + const song = queue.next(); + expect(song?.id).toBe("B"); + expect(queue.next()).toBeNull(); + }); + it("random-loop mode never returns null", () => { queue.setMode(PlayMode.RandomLoop); queue.add(makeSong("1")); diff --git a/src/audio/queue.ts b/src/audio/queue.ts index 7a6f36f..b97444d 100644 --- a/src/audio/queue.ts +++ b/src/audio/queue.ts @@ -61,6 +61,7 @@ export class PlayQueue { if (this.songs.length === 0) return null; this.playedIndices.clear(); this.currentIndex = 0; + this.playedIndices.add(0); return this.songs[0]; } @@ -68,6 +69,7 @@ export class PlayQueue { if (index < 0 || index >= this.songs.length) return null; this.playedIndices.clear(); this.currentIndex = index; + this.playedIndices.add(index); return this.songs[index]; } @@ -86,9 +88,6 @@ export class PlayQueue { return this.songs[this.currentIndex]; } case PlayMode.Random: { - if (this.currentIndex >= 0) { - this.playedIndices.add(this.currentIndex); - } const unplayed: number[] = []; for (let i = 0; i < this.songs.length; i++) { if (!this.playedIndices.has(i)) unplayed.push(i); @@ -97,6 +96,7 @@ export class PlayQueue { const nextIndex = unplayed[Math.floor(Math.random() * unplayed.length)]; this.currentIndex = nextIndex; + this.playedIndices.add(nextIndex); return this.songs[nextIndex]; } case PlayMode.RandomLoop: { @@ -124,6 +124,7 @@ export class PlayQueue { } else { this.currentIndex = prevIndex; } + this.playedIndices.add(this.currentIndex); return this.songs[this.currentIndex]; } @@ -152,6 +153,9 @@ export class PlayQueue { setMode(mode: PlayMode): void { this.mode = mode; this.playedIndices.clear(); + if (this.currentIndex >= 0) { + this.playedIndices.add(this.currentIndex); + } } getCurrentIndex(): number { From 010aa8aa8cd85fa278ca626a0c870db6dd285a48 Mon Sep 17 00:00:00 2001 From: TIANYAO ZHANG <88520881+ZHANGTIANYAO1@users.noreply.github.com> Date: Mon, 13 Apr 2026 00:11:41 +0800 Subject: [PATCH 06/11] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b20a224..d62856f 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,8 @@ ## 截图 -> *截图即将添加* +> musicbot1 +> musicbot2 ## 快速开始 From da5b34f5f49925c4a84c79c0f5903c56ca7161c0 Mon Sep 17 00:00:00 2001 From: saopig1 Date: Mon, 13 Apr 2026 00:14:29 +0800 Subject: [PATCH 07/11] feat(profile): auto-update bot avatar, nickname, and away status based on playing song MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add BotProfileManager that updates the bot's TeamSpeak presence when songs change: album cover as avatar, song info in nickname, away status toggled on stop/play. Each feature is independently configurable via REST API and persisted to the database. Permission-safe — features that fail due to insufficient server permissions are silently disabled until reconnect. Description falls back to nickname display on TS3 (only supported via TS6 HTTP Query). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../specs/2026-04-12-bot-profile-design.md | 124 +++++++ src/bot/instance.ts | 29 +- src/bot/profile.ts | 333 ++++++++++++++++++ src/data/database.ts | 77 ++++ src/ts-protocol/client.ts | 66 ++++ src/web/api/player.ts | 21 ++ 6 files changed, 649 insertions(+), 1 deletion(-) create mode 100644 docs/superpowers/specs/2026-04-12-bot-profile-design.md create mode 100644 src/bot/profile.ts diff --git a/docs/superpowers/specs/2026-04-12-bot-profile-design.md b/docs/superpowers/specs/2026-04-12-bot-profile-design.md new file mode 100644 index 0000000..10eb423 --- /dev/null +++ b/docs/superpowers/specs/2026-04-12-bot-profile-design.md @@ -0,0 +1,124 @@ +# Bot Profile Manager — Design Spec + +## Goal + +When the bot plays a song, automatically update its TeamSpeak presence (avatar, description, nickname, away status, channel description) and send a "now playing" chat message. When playback stops, restore all values to defaults. Each feature is independently configurable and permission-safe — if the bot lacks a required server permission, that feature silently disables itself until the next reconnect. + +## Features + +| # | Feature | Update on song | Restore on stop | TS3 mechanism | TS6 mechanism | +|---|---------|---------------|-----------------|---------------|---------------| +| 1 | Avatar | Album cover art | Delete avatar | File transfer upload + `clientupdate client_flag_avatar=` | Same (file transfer is protocol-level) | +| 2 | Description | `歌名 - 歌手 [专辑]` | Clear (empty string) | `clientupdate client_description=...` | `httpQuery.clientUpdate(...)` | +| 3 | Nickname | `♪ 歌名 - 歌手 \| 原昵称` (max 30 chars) | Restore `defaultNickname` | `clientupdate client_nickname=...` | `httpQuery.clientUpdate(...)` | +| 4 | Away status | `client_away=1`, message = `正在播放: 歌名 - 歌手` | `client_away=0` | `clientupdate client_away=...` | `httpQuery.clientUpdate(...)` | +| 5 | Channel description | `正在播放: 歌名 - 歌手\n专辑: xxx\n平台: xxx` | Clear | `channeledit cid=... channel_description=...` | `httpQuery.request("POST", "/1/channeledit", ...)` | +| 6 | Now-playing message | `♪ 正在播放: 歌名 - 歌手 [专辑]` | (not sent on stop) | `sendTextMessage` (existing) | `sendTextMessage` (existing) | + +## Architecture + +``` +resolveAndPlay() success / stop / clear / playNext exhausted + ↓ +BotInstance → BotProfileManager.onSongChange(song | null) + ├─ updateAvatar(coverUrl | null) [fire-and-forget] + ├─ updateDescription(song | null) + ├─ updateNickname(song | null) + ├─ updateAwayStatus(song | null) + ├─ updateChannelDescription(song | null) + └─ sendNowPlayingMessage(song) [only when song != null] + +BotInstance.connect() → profileManager.onConnect() // reset perm flags, restore defaults +BotInstance.disconnect() → (no action needed, server cleans up) +``` + +## File Changes + +| File | Change | +|------|--------| +| `src/bot/profile.ts` | **New** — `BotProfileManager` class | +| `src/ts-protocol/client.ts` | Add `execCommand`, `execCommandWithResponse`, file transfer methods, `escapeTS3()` | +| `src/bot/instance.ts` | Create & hold `BotProfileManager`, call at lifecycle points | +| `src/data/database.ts` | Add 6 profile config columns via ALTER TABLE migration | +| `src/web/api/player.ts` | Add `GET/PUT /api/player/:botId/profile` endpoints | + +## TS3Client Layer Extensions + +New methods on `TS3Client` (all delegate to underlying `@honeybbq/teamspeak-client` Client): + +```typescript +execCommand(cmd: string): Promise +execCommandWithResponse(cmd: string): Promise[]> +fileTransferInitUpload(channelID: bigint, path: string, password: string, + size: bigint, overwrite?: boolean): Promise +uploadFileData(host: string, info: FileUploadInfo, data: Readable): Promise +fileTransferDeleteFile(channelID: bigint, paths: string[]): Promise +``` + +Utility: `escapeTS3(str: string): string` — escapes spaces (`\s`), backslashes (`\\`), pipes (`\p`), slashes (`\/`). + +## BotProfileManager Detail + +```typescript +interface ProfileConfig { + avatarEnabled: boolean; // default true + descriptionEnabled: boolean; // default true + nicknameEnabled: boolean; // default true + awayStatusEnabled: boolean; // default true + channelDescEnabled: boolean; // default true + nowPlayingMsgEnabled: boolean; // default true +} +``` + +### Permission Handling + +- Each feature has an independent `permDenied: boolean` flag. +- On first failure where error message contains "permission" or "insufficient" → set flag, skip subsequent calls. +- On `onConnect()` → reset all flags (new connection may have different permissions). +- Non-permission errors (network timeout, etc.) do NOT set the flag — next song change will retry. + +### Nickname Truncation + +- Format: `♪ {songInfo} | {defaultNickname}` +- TS3 max nickname: 30 characters +- If total > 30: truncate songInfo, keep defaultNickname +- If `♪ | {defaultNickname}` alone > 30: skip nickname update entirely + +### Avatar Upload Flow (TS3) + +1. Download cover image via axios (HTTP GET coverUrl) → Buffer +2. `fileTransferInitUpload(0n, "/avatar", "", BigInt(buffer.length), true)` +3. `uploadFileData(host, info, Readable.from(buffer))` +4. Compute MD5: `crypto.createHash('md5').update(buffer).digest('hex')` +5. `execCommand("clientupdate client_flag_avatar=" + md5)` + +To clear: `fileTransferDeleteFile(0n, ["/avatar"])` + `execCommand("clientupdate client_flag_avatar=")` + +### Fire-and-Forget + +Avatar download/upload is slow. `onSongChange()` launches all updates concurrently via `Promise.allSettled()` — failures are logged but never block playback. + +## Database Migration + +```sql +ALTER TABLE bot_instances ADD COLUMN profile_avatar_enabled INTEGER DEFAULT 1; +ALTER TABLE bot_instances ADD COLUMN profile_description_enabled INTEGER DEFAULT 1; +ALTER TABLE bot_instances ADD COLUMN profile_nickname_enabled INTEGER DEFAULT 1; +ALTER TABLE bot_instances ADD COLUMN profile_away_enabled INTEGER DEFAULT 1; +ALTER TABLE bot_instances ADD COLUMN profile_channel_desc_enabled INTEGER DEFAULT 1; +ALTER TABLE bot_instances ADD COLUMN profile_now_playing_enabled INTEGER DEFAULT 1; +``` + +## Web API + +``` +GET /api/player/:botId/profile → { avatarEnabled, descriptionEnabled, ... } +PUT /api/player/:botId/profile → body: Partial → 200 OK +``` + +## Constraints + +- All profile operations are async, never block playback +- Uses existing `axios` dependency for image download +- MD5 via Node.js built-in `crypto` +- No new npm dependencies required diff --git a/src/bot/instance.ts b/src/bot/instance.ts index 58192bb..3b1f49a 100644 --- a/src/bot/instance.ts +++ b/src/bot/instance.ts @@ -13,8 +13,9 @@ import { type ParsedCommand, } from "./commands.js"; import type { Logger } from "../logger.js"; -import type { BotDatabase } from "../data/database.js"; +import type { BotDatabase, ProfileConfig } from "../data/database.js"; import type { BotConfig } from "../data/config.js"; +import { BotProfileManager } from "./profile.js"; export interface BotInstanceOptions { id: string; @@ -60,6 +61,7 @@ export class BotInstance extends EventEmitter { private disconnectEmitted = false; private voteSkipUsers = new Set(); private isAdvancing = false; + private profileManager: BotProfileManager; constructor(options: BotInstanceOptions) { super(); @@ -77,6 +79,14 @@ export class BotInstance extends EventEmitter { this.player = new AudioPlayer(this.logger); this.queue = new PlayQueue(); + const profileConfig = this.database.getProfileConfig(this.id); + this.profileManager = new BotProfileManager( + this.tsClient, + this.logger, + profileConfig, + options.tsOptions.nickname, + ); + this.setupPlayerEvents(); this.setupTsEvents(); } @@ -134,6 +144,7 @@ export class BotInstance extends EventEmitter { throw new Error("Connect aborted by concurrent disconnect"); } this.connected = true; + this.profileManager.onConnect(); this.emit("connected"); } @@ -306,6 +317,10 @@ export class BotInstance extends EventEmitter { platform: song.platform, coverUrl: song.coverUrl, }); + // Update bot presence (fire-and-forget — never blocks playback) + this.profileManager.onSongChange(song).catch((err) => { + this.logger.warn({ err }, "Profile update failed after song change"); + }); this.emit("stateChange"); return true; } catch (err) { @@ -374,6 +389,9 @@ export class BotInstance extends EventEmitter { private cmdStop(): string { this.player.stop(); this.queue.clear(); + this.profileManager.onSongChange(null).catch((err) => { + this.logger.warn({ err }, "Profile restore failed on stop"); + }); this.emit("stateChange"); return "Stopped and queue cleared"; } @@ -424,6 +442,9 @@ export class BotInstance extends EventEmitter { private cmdClear(): string { this.player.stop(); this.queue.clear(); + this.profileManager.onSongChange(null).catch((err) => { + this.logger.warn({ err }, "Profile restore failed on clear"); + }); this.emit("stateChange"); return "Queue cleared"; } @@ -590,9 +611,11 @@ export class BotInstance extends EventEmitter { } if (!started) { this.player.stop(); + this.profileManager.onSongChange(null).catch(() => {}); } } else { this.player.stop(); + this.profileManager.onSongChange(null).catch(() => {}); } this.emit("stateChange"); } finally { @@ -639,6 +662,10 @@ export class BotInstance extends EventEmitter { return this.connected; } + getProfileManager(): BotProfileManager { + return this.profileManager; + } + getIdentityExport(): string | undefined { return this.tsClient.getIdentityExport(); } diff --git a/src/bot/profile.ts b/src/bot/profile.ts new file mode 100644 index 0000000..6d8f5c3 --- /dev/null +++ b/src/bot/profile.ts @@ -0,0 +1,333 @@ +import { createHash } from "node:crypto"; +import { Readable } from "node:stream"; +import axios from "axios"; +import { TS3Client, escapeTS3 } from "../ts-protocol/client.js"; +import type { ProfileConfig } from "../data/database.js"; +import type { QueuedSong } from "../audio/queue.js"; +import type { Logger } from "../logger.js"; + +const TS3_NICKNAME_MAX = 30; +/** TS3 avatar max size — server default is ~300 KB. Use 200 KB to be safe. */ +const AVATAR_MAX_BYTES = 200 * 1024; + +/** + * Manages the bot's TeamSpeak presence (avatar, description, nickname, + * away status, channel description, now-playing messages). + * + * Every update is permission-safe: if a feature fails due to insufficient + * server permissions, it silently disables itself until the next reconnect. + */ +export class BotProfileManager { + private tsClient: TS3Client; + private logger: Logger; + private config: ProfileConfig; + private defaultNickname: string; + + /** Per-feature permission-denied flags. Reset on reconnect. */ + private permDenied = { + avatar: false, + description: false, + nickname: false, + awayStatus: false, + channelDesc: false, + nowPlayingMsg: false, + }; + + constructor( + tsClient: TS3Client, + logger: Logger, + config: ProfileConfig, + defaultNickname: string, + ) { + this.tsClient = tsClient; + this.logger = logger.child({ component: "profile" }); + this.config = { ...config }; + this.defaultNickname = defaultNickname; + } + + // --- Public API --- + + /** + * Called when a new song starts playing (song != null) or playback + * stops (song == null). + * + * Commands are serialized to avoid overwhelming the TS3 command queue. + * Nickname + away status are merged into a single `clientupdate` call. + */ + async onSongChange(song: QueuedSong | null): Promise { + // 1. Avatar first — file transfer uses its own response tracker and + // must run before sendCommandNoWait calls whose orphaned responses + // could confuse the command matcher. + await this.updateAvatar(song?.coverUrl ?? null); + // 2. Combined clientupdate (nickname + away in one fire-and-forget) + await this.updateClientProperties(song); + // 3. Description (TS6 only — instant skip on TS3) + await this.updateDescription(song); + // 4. Channel description (fire-and-forget channeledit) + await this.updateChannelDescription(song); + // 5. Now-playing chat message + if (song) await this.sendNowPlayingMessage(song); + } + + /** Reset permission-denied flags on new connection. */ + onConnect(): void { + this.permDenied = { + avatar: false, + description: false, + nickname: false, + awayStatus: false, + channelDesc: false, + nowPlayingMsg: false, + }; + } + + getConfig(): ProfileConfig { + return { ...this.config }; + } + + updateConfig(partial: Partial): void { + Object.assign(this.config, partial); + } + + // --- Internal update methods --- + + private async updateAvatar(coverUrl: string | null): Promise { + if (!this.config.avatarEnabled || this.permDenied.avatar) return; + try { + if (!coverUrl) { + await this.clearAvatar(); + return; + } + // Request a thumbnail from the CDN to stay within TS3's avatar size limit. + const thumbUrl = this.thumbnailUrl(coverUrl); + const imageBuffer = await this.downloadImage(thumbUrl); + if (!imageBuffer || imageBuffer.length === 0) return; + if (imageBuffer.length > AVATAR_MAX_BYTES) { + this.logger.warn( + { bytes: imageBuffer.length, max: AVATAR_MAX_BYTES }, + "Cover image still too large after resize — skipping avatar update", + ); + return; + } + + // Wrap the file-transfer sequence with a 6s timeout — the TS3 + // full-client file transfer can silently hang. + await Promise.race([ + this.doAvatarUpload(imageBuffer), + new Promise((_, reject) => + setTimeout(() => reject(new Error("Avatar upload timed out")), 6000), + ), + ]); + this.logger.info("Avatar updated"); + } catch (err) { + this.handleFeatureError("avatar", err); + } + } + + private async doAvatarUpload(imageBuffer: Buffer): Promise { + const host = this.tsClient.getHost(); + const info = await this.tsClient.fileTransferInitUpload( + 0n, "/avatar", "", BigInt(imageBuffer.length), true, + ); + await this.tsClient.uploadFileData(host, info, Readable.from(imageBuffer)); + const md5 = createHash("md5").update(imageBuffer).digest("hex"); + await this.tsClient.sendCommandNoWait(`clientupdate client_flag_avatar=${escapeTS3(md5)}`); + } + + private async clearAvatar(): Promise { + try { + await this.tsClient.fileTransferDeleteFile(0n, ["/avatar"]); + } catch { + // File may not exist — that's fine + } + try { + await this.tsClient.sendCommandNoWait("clientupdate client_flag_avatar="); + } catch (err) { + this.handleFeatureError("avatar", err); + } + } + + private async updateDescription(song: QueuedSong | null): Promise { + if (!this.config.descriptionEnabled || this.permDenied.description) return; + + // TS3 full-client protocol rejects client_description in clientupdate + // (error 1538 "invalid parameter"). Only TS6 HTTP Query supports it. + // On TS3 we rely on nickname to display song info instead. + const httpQuery = this.tsClient.getHttpQuery(); + if (!httpQuery) { + this.permDenied.description = true; + this.logger.info( + "Description not available on TS3 — using nickname to display song info", + ); + return; + } + + try { + const text = song + ? `${song.name} - ${song.artist} [${song.album}]` + : ""; + await httpQuery.clientUpdate({ client_description: text }); + this.logger.info("Description updated"); + } catch (err) { + this.handleFeatureError("description", err); + } + } + + /** + * Build and send a single `clientupdate` command that sets nickname + * and away status together, avoiding multiple round-trips that can + * cause command-queue timeouts on the TS3 protocol. + */ + private async updateClientProperties(song: QueuedSong | null): Promise { + const parts: string[] = []; + + // --- Nickname --- + if (this.config.nicknameEnabled && !this.permDenied.nickname) { + if (!song) { + parts.push(`client_nickname=${escapeTS3(this.defaultNickname)}`); + } else { + const songInfo = `${song.name} - ${song.artist}`; + const prefix = "\u266A "; // ♪ + const sep = " - "; + const overhead = prefix.length + sep.length + this.defaultNickname.length; + if (overhead <= TS3_NICKNAME_MAX) { + const maxSongLen = TS3_NICKNAME_MAX - overhead; + const truncated = songInfo.length > maxSongLen + ? songInfo.slice(0, maxSongLen - 1) + "\u2026" + : songInfo; + parts.push(`client_nickname=${escapeTS3(`${prefix}${truncated}${sep}${this.defaultNickname}`)}`); + } + } + } + + // --- Away status --- + if (this.config.awayStatusEnabled && !this.permDenied.awayStatus) { + if (song) { + parts.push("client_away=0"); + } else { + parts.push(`client_away=1 client_away_message=${escapeTS3("\u7B49\u5F85\u64AD\u653E")}`); + } + } + + if (parts.length === 0) return; + + try { + const httpQuery = this.tsClient.getHttpQuery(); + if (httpQuery) { + // TS6: build a properties object + const props: Record = {}; + for (const part of parts) { + const eq = part.indexOf("="); + if (eq > 0) props[part.slice(0, eq)] = part.slice(eq + 1); + } + await httpQuery.clientUpdate(props); + } else { + // Use sendCommandNoWait: the TS3 full-client protocol often + // doesn't return a timely error response for clientupdate, + // causing execCommand to time out after 10s. + await this.tsClient.sendCommandNoWait(`clientupdate ${parts.join(" ")}`); + } + this.logger.info("Client properties updated (nickname + away)"); + } catch (err) { + // Flag both features on permission error + this.handleFeatureError("nickname", err); + this.handleFeatureError("awayStatus", err); + } + } + + private async updateChannelDescription(song: QueuedSong | null): Promise { + if (!this.config.channelDescEnabled || this.permDenied.channelDesc) return; + try { + const channelId = this.tsClient.getChannelId(); + if (channelId === 0n) return; // unknown channel + + if (!song) { + await this.tsClient.sendCommandNoWait( + `channeledit cid=${channelId} channel_description=`, + ); + this.logger.debug("Channel description cleared"); + return; + } + + const lines = [ + `\u266A \u6B63\u5728\u64AD\u653E: ${song.name} - ${song.artist}`, // ♪ 正在播放: + `\u4E13\u8F91: ${song.album}`, // 专辑: + `\u5E73\u53F0: ${song.platform}`, // 平台: + ]; + const desc = lines.join("\\n"); + await this.tsClient.sendCommandNoWait( + `channeledit cid=${channelId} channel_description=${escapeTS3(desc)}`, + ); + this.logger.debug("Channel description updated"); + } catch (err) { + this.handleFeatureError("channelDesc", err); + } + } + + private async sendNowPlayingMessage(song: QueuedSong): Promise { + if (!this.config.nowPlayingMsgEnabled || this.permDenied.nowPlayingMsg) return; + try { + const text = `\u266A \u6B63\u5728\u64AD\u653E: ${song.name} - ${song.artist} [${song.album}]`; + await this.tsClient.sendTextMessage(text); + this.logger.debug("Now-playing message sent"); + } catch (err) { + this.handleFeatureError("nowPlayingMsg", err); + } + } + + // --- Helpers --- + + /** + * Append CDN resize parameters to get a thumbnail suitable for TS3 avatars. + * NetEase and QQ Music CDNs both support URL-based image resizing. + */ + private thumbnailUrl(url: string): string { + if (url.includes("music.126.net") || url.includes("netease")) { + // NetEase Cloud Music CDN: ?param=y + return url.includes("?") ? url : `${url}?param=200y200`; + } + if (url.includes("qqmusic") || url.includes("qq.com")) { + // QQ Music CDN: /200 suffix or similar + return url.replace(/\/\d+$/, "/200"); + } + // Other platforms — return as-is, size check will skip if too large + return url; + } + + private async downloadImage(url: string): Promise { + try { + const resp = await axios.get(url, { + responseType: "arraybuffer", + timeout: 8000, + maxContentLength: 2 * 1024 * 1024, // 2 MB cap + }); + return Buffer.from(resp.data); + } catch (err) { + this.logger.warn({ err, url }, "Failed to download cover image"); + return null; + } + } + + private handleFeatureError( + feature: keyof typeof this.permDenied, + err: unknown, + ): void { + const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase(); + // Disable the feature for this session on unrecoverable errors: + // - permission / insufficient → server denies the action + // - invalid parameter → command not supported by this protocol + if ( + msg.includes("permission") || + msg.includes("insufficient") || + msg.includes("invalid parameter") + ) { + this.permDenied[feature] = true; + this.logger.info( + { feature, reason: msg }, + "Feature disabled for this session (will retry after reconnect)", + ); + } else { + this.logger.warn({ feature, err }, "Profile update failed"); + } + } +} diff --git a/src/data/database.ts b/src/data/database.ts index 8e193e1..3ca4631 100644 --- a/src/data/database.ts +++ b/src/data/database.ts @@ -33,6 +33,24 @@ export interface BotInstance { identity?: string; } +export interface ProfileConfig { + avatarEnabled: boolean; + descriptionEnabled: boolean; + nicknameEnabled: boolean; + awayStatusEnabled: boolean; + channelDescEnabled: boolean; + nowPlayingMsgEnabled: boolean; +} + +export const DEFAULT_PROFILE_CONFIG: ProfileConfig = { + avatarEnabled: true, + descriptionEnabled: true, + nicknameEnabled: true, + awayStatusEnabled: true, + channelDescEnabled: true, + nowPlayingMsgEnabled: true, +}; + export interface BotDatabase { db: Database.Database; addPlayHistory(entry: PlayHistoryEntry): void; @@ -40,6 +58,8 @@ export interface BotDatabase { saveBotInstance(instance: BotInstance): void; getBotInstances(): BotInstance[]; deleteBotInstance(id: string): boolean; + getProfileConfig(botId: string): ProfileConfig; + saveProfileConfig(botId: string, config: ProfileConfig): void; close(): void; } @@ -58,6 +78,20 @@ function migrateSchema(db: Database.Database): void { if (!names.includes("serverPassword")) { db.exec("ALTER TABLE bot_instances ADD COLUMN serverPassword TEXT NOT NULL DEFAULT ''"); } + // Profile feature flags + const profileCols = [ + "profile_avatar_enabled", + "profile_description_enabled", + "profile_nickname_enabled", + "profile_away_enabled", + "profile_channel_desc_enabled", + "profile_now_playing_enabled", + ]; + for (const col of profileCols) { + if (!names.includes(col)) { + db.exec(`ALTER TABLE bot_instances ADD COLUMN ${col} INTEGER NOT NULL DEFAULT 1`); + } + } } function initTables(db: Database.Database): void { @@ -127,6 +161,24 @@ export function createDatabase(dbPath: string): BotDatabase { const deleteInstance = db.prepare(`DELETE FROM bot_instances WHERE id = ?`); + const selectProfileConfig = db.prepare(` + SELECT profile_avatar_enabled, profile_description_enabled, + profile_nickname_enabled, profile_away_enabled, + profile_channel_desc_enabled, profile_now_playing_enabled + FROM bot_instances WHERE id = ? + `); + + const updateProfileConfig = db.prepare(` + UPDATE bot_instances SET + profile_avatar_enabled = @avatar, + profile_description_enabled = @description, + profile_nickname_enabled = @nickname, + profile_away_enabled = @away, + profile_channel_desc_enabled = @channelDesc, + profile_now_playing_enabled = @nowPlaying + WHERE id = @id + `); + return { db, @@ -165,6 +217,31 @@ export function createDatabase(dbPath: string): BotDatabase { return result.changes > 0; }, + getProfileConfig(botId) { + const row = selectProfileConfig.get(botId) as Record | undefined; + if (!row) return { ...DEFAULT_PROFILE_CONFIG }; + return { + avatarEnabled: row.profile_avatar_enabled === 1, + descriptionEnabled: row.profile_description_enabled === 1, + nicknameEnabled: row.profile_nickname_enabled === 1, + awayStatusEnabled: row.profile_away_enabled === 1, + channelDescEnabled: row.profile_channel_desc_enabled === 1, + nowPlayingMsgEnabled: row.profile_now_playing_enabled === 1, + }; + }, + + saveProfileConfig(botId, config) { + updateProfileConfig.run({ + id: botId, + avatar: config.avatarEnabled ? 1 : 0, + description: config.descriptionEnabled ? 1 : 0, + nickname: config.nicknameEnabled ? 1 : 0, + away: config.awayStatusEnabled ? 1 : 0, + channelDesc: config.channelDescEnabled ? 1 : 0, + nowPlaying: config.nowPlayingMsgEnabled ? 1 : 0, + }); + }, + close() { db.close(); }, diff --git a/src/ts-protocol/client.ts b/src/ts-protocol/client.ts index fdf2f1a..b597317 100644 --- a/src/ts-protocol/client.ts +++ b/src/ts-protocol/client.ts @@ -1,4 +1,5 @@ import { EventEmitter } from "node:events"; +import { Readable } from "node:stream"; import { Client as TS3FullClient, generateIdentity as genTS3Identity, @@ -7,9 +8,11 @@ import { listChannels, listClients, clientMove, + fileTransferDeleteFile, type Identity, type TextMessage, type ClientInfo, + type FileUploadInfo, } from "@honeybbq/teamspeak-client"; import type { Logger } from "../logger.js"; import { @@ -20,6 +23,19 @@ import { TS6HttpQuery } from "./http-query.js"; export { CODEC_OPUS_MUSIC } from "./voice.js"; export type { ServerProtocol } from "./protocol-detect.js"; +export type { FileUploadInfo } from "@honeybbq/teamspeak-client"; + +/** Escape a string for use in TS3 ServerQuery-style commands. */ +export function escapeTS3(str: string): string { + return str + .replace(/\\/g, "\\\\") + .replace(/ /g, "\\s") + .replace(/\//g, "\\/") + .replace(/\|/g, "\\p") + .replace(/\t/g, "\\t") + .replace(/\n/g, "\\n") + .replace(/\r/g, "\\r"); +} export interface TS3ClientOptions { host: string; @@ -278,6 +294,56 @@ export class TS3Client extends EventEmitter { } } + // --- Raw command & file transfer pass-through --- + + async execCommand(cmd: string): Promise { + if (!this.client) throw new Error("Not connected"); + await this.client.execCommand(cmd); + } + + /** Fire a command without waiting for the server's response. */ + async sendCommandNoWait(cmd: string): Promise { + if (!this.client) throw new Error("Not connected"); + await this.client.sendCommandNoWait(cmd); + } + + async execCommandWithResponse(cmd: string): Promise[]> { + if (!this.client) throw new Error("Not connected"); + return this.client.execCommandWithResponse(cmd); + } + + async fileTransferInitUpload( + channelID: bigint, + path: string, + password: string, + size: bigint, + overwrite = true, + ): Promise { + if (!this.client) throw new Error("Not connected"); + return this.client.fileTransferInitUpload(channelID, path, password, size, overwrite); + } + + async uploadFileData(host: string, info: FileUploadInfo, data: Readable): Promise { + if (!this.client) throw new Error("Not connected"); + await this.client.uploadFileData(host, info, data); + } + + async fileTransferDeleteFile(channelID: bigint, paths: string[]): Promise { + if (!this.client) throw new Error("Not connected"); + await fileTransferDeleteFile(this.client, channelID, paths); + } + + /** The server host (needed for file transfer TCP connections). */ + getHost(): string { + return this.options.host; + } + + /** The current channel ID of this client. */ + getChannelId(): bigint { + if (!this.client) return 0n; + return this.client.channelID(); + } + private voiceFramesSent = 0; sendVoiceData(opusFrame: Buffer): void { diff --git a/src/web/api/player.ts b/src/web/api/player.ts index 4cd55b3..330ea49 100644 --- a/src/web/api/player.ts +++ b/src/web/api/player.ts @@ -343,6 +343,27 @@ export function createPlayerRouter( } }); + // --- Profile config endpoints --- + + router.get("/:botId/profile", (req, res) => { + const bot = (req as any).bot; + res.json(bot.getProfileManager().getConfig()); + }); + + router.put("/:botId/profile", (req, res) => { + try { + const bot = (req as any).bot; + const pm = bot.getProfileManager(); + pm.updateConfig(req.body); + if (database) { + database.saveProfileConfig(bot.id, pm.getConfig()); + } + res.json(pm.getConfig()); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } + }); + router.get("/:botId/history", (req, res) => { if (!database) { res.json({ history: [] }); From 30aced6d36c6271c7b2c945423d07e85f9a321e3 Mon Sep 17 00:00:00 2001 From: saopig1 Date: Mon, 13 Apr 2026 00:39:36 +0800 Subject: [PATCH 08/11] fix(profile): use clientedit for description, add sendCommandNoWait Description via clientupdate is rejected (error 1538) on TS3 full-client protocol. Switch to clientedit on the bot's own clid, which is how TS3AudioBot handles it. Requires b_client_modify_description permission. Also add sendCommandNoWait to TS3Client for fire-and-forget commands (clientupdate, channeledit) that don't return timely responses. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/bot/profile.ts | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/bot/profile.ts b/src/bot/profile.ts index 6d8f5c3..6ff3968 100644 --- a/src/bot/profile.ts +++ b/src/bot/profile.ts @@ -149,24 +149,23 @@ export class BotProfileManager { private async updateDescription(song: QueuedSong | null): Promise { if (!this.config.descriptionEnabled || this.permDenied.description) return; - - // TS3 full-client protocol rejects client_description in clientupdate - // (error 1538 "invalid parameter"). Only TS6 HTTP Query supports it. - // On TS3 we rely on nickname to display song info instead. - const httpQuery = this.tsClient.getHttpQuery(); - if (!httpQuery) { - this.permDenied.description = true; - this.logger.info( - "Description not available on TS3 — using nickname to display song info", - ); - return; - } - try { const text = song ? `${song.name} - ${song.artist} [${song.album}]` : ""; - await httpQuery.clientUpdate({ client_description: text }); + const httpQuery = this.tsClient.getHttpQuery(); + if (httpQuery) { + await httpQuery.clientUpdate({ client_description: text }); + } else { + // clientupdate rejects client_description (error 1538). + // Use clientedit on our own clid instead — this is what + // TS3AudioBot does via TSLib's ChangeDescription(). + const clid = this.tsClient.getClientId(); + if (clid <= 0) return; + await this.tsClient.execCommand( + `clientedit clid=${clid} client_description=${escapeTS3(text)}`, + ); + } this.logger.info("Description updated"); } catch (err) { this.handleFeatureError("description", err); From b4ad78ce84ab31ce5850947070fcdcd221c2f967 Mon Sep 17 00:00:00 2001 From: saopig1 Date: Mon, 13 Apr 2026 00:44:57 +0800 Subject: [PATCH 09/11] fix(profile): harden against race conditions and edge cases - Add generation counter to prevent stale avatar uploads from overwriting newer song's profile when rapidly skipping tracks - Fix nickname truncation to use UTF-8 byte length instead of JS string length (TS3 counts bytes, Chinese chars are 3 bytes) - Add timeout to clearAvatar file transfer (was missing) - Extract withTimeout helper to deduplicate timeout logic - Add BiliBili CDN thumbnail resize support (@200w_200h) - Bump generation on reconnect to discard in-flight updates from the old connection Co-Authored-By: Claude Opus 4.6 (1M context) --- src/bot/profile.ts | 128 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 98 insertions(+), 30 deletions(-) diff --git a/src/bot/profile.ts b/src/bot/profile.ts index 6ff3968..f8aeb48 100644 --- a/src/bot/profile.ts +++ b/src/bot/profile.ts @@ -9,6 +9,8 @@ import type { Logger } from "../logger.js"; const TS3_NICKNAME_MAX = 30; /** TS3 avatar max size — server default is ~300 KB. Use 200 KB to be safe. */ const AVATAR_MAX_BYTES = 200 * 1024; +/** Timeout for file-transfer operations (upload / delete). */ +const FILE_TRANSFER_TIMEOUT_MS = 6000; /** * Manages the bot's TeamSpeak presence (avatar, description, nickname, @@ -33,6 +35,14 @@ export class BotProfileManager { nowPlayingMsg: false, }; + /** + * Monotonically increasing generation counter. Incremented on every + * onSongChange / onConnect call. Long-running operations (avatar + * download/upload) check this before committing their result — if + * the generation changed, a newer update has superseded them. + */ + private generation = 0; + constructor( tsClient: TS3Client, logger: Logger, @@ -53,15 +63,23 @@ export class BotProfileManager { * * Commands are serialized to avoid overwhelming the TS3 command queue. * Nickname + away status are merged into a single `clientupdate` call. + * + * A generation counter guards against stale updates: if a newer + * onSongChange fires while the avatar is still downloading, the old + * update is discarded. */ async onSongChange(song: QueuedSong | null): Promise { + const gen = ++this.generation; + // 1. Avatar first — file transfer uses its own response tracker and // must run before sendCommandNoWait calls whose orphaned responses // could confuse the command matcher. - await this.updateAvatar(song?.coverUrl ?? null); + await this.updateAvatar(song?.coverUrl ?? null, gen); + if (this.generation !== gen) return; // superseded + // 2. Combined clientupdate (nickname + away in one fire-and-forget) await this.updateClientProperties(song); - // 3. Description (TS6 only — instant skip on TS3) + // 3. Description (clientedit on TS3, httpQuery on TS6) await this.updateDescription(song); // 4. Channel description (fire-and-forget channeledit) await this.updateChannelDescription(song); @@ -69,8 +87,9 @@ export class BotProfileManager { if (song) await this.sendNowPlayingMessage(song); } - /** Reset permission-denied flags on new connection. */ + /** Reset permission-denied flags and bump generation on new connection. */ onConnect(): void { + this.generation++; this.permDenied = { avatar: false, description: false, @@ -91,7 +110,7 @@ export class BotProfileManager { // --- Internal update methods --- - private async updateAvatar(coverUrl: string | null): Promise { + private async updateAvatar(coverUrl: string | null, gen: number): Promise { if (!this.config.avatarEnabled || this.permDenied.avatar) return; try { if (!coverUrl) { @@ -101,6 +120,10 @@ export class BotProfileManager { // Request a thumbnail from the CDN to stay within TS3's avatar size limit. const thumbUrl = this.thumbnailUrl(coverUrl); const imageBuffer = await this.downloadImage(thumbUrl); + + // Check generation after the slow download — bail if superseded. + if (this.generation !== gen) return; + if (!imageBuffer || imageBuffer.length === 0) return; if (imageBuffer.length > AVATAR_MAX_BYTES) { this.logger.warn( @@ -110,14 +133,9 @@ export class BotProfileManager { return; } - // Wrap the file-transfer sequence with a 6s timeout — the TS3 + // Wrap the file-transfer sequence with a timeout — the TS3 // full-client file transfer can silently hang. - await Promise.race([ - this.doAvatarUpload(imageBuffer), - new Promise((_, reject) => - setTimeout(() => reject(new Error("Avatar upload timed out")), 6000), - ), - ]); + await this.withTimeout(this.doAvatarUpload(imageBuffer), FILE_TRANSFER_TIMEOUT_MS); this.logger.info("Avatar updated"); } catch (err) { this.handleFeatureError("avatar", err); @@ -136,9 +154,12 @@ export class BotProfileManager { private async clearAvatar(): Promise { try { - await this.tsClient.fileTransferDeleteFile(0n, ["/avatar"]); + await this.withTimeout( + this.tsClient.fileTransferDeleteFile(0n, ["/avatar"]), + FILE_TRANSFER_TIMEOUT_MS, + ); } catch { - // File may not exist — that's fine + // File may not exist or transfer timed out — that's fine } try { await this.tsClient.sendCommandNoWait("clientupdate client_flag_avatar="); @@ -185,16 +206,9 @@ export class BotProfileManager { if (!song) { parts.push(`client_nickname=${escapeTS3(this.defaultNickname)}`); } else { - const songInfo = `${song.name} - ${song.artist}`; - const prefix = "\u266A "; // ♪ - const sep = " - "; - const overhead = prefix.length + sep.length + this.defaultNickname.length; - if (overhead <= TS3_NICKNAME_MAX) { - const maxSongLen = TS3_NICKNAME_MAX - overhead; - const truncated = songInfo.length > maxSongLen - ? songInfo.slice(0, maxSongLen - 1) + "\u2026" - : songInfo; - parts.push(`client_nickname=${escapeTS3(`${prefix}${truncated}${sep}${this.defaultNickname}`)}`); + const nickname = this.buildNickname(song); + if (nickname) { + parts.push(`client_nickname=${escapeTS3(nickname)}`); } } } @@ -234,6 +248,51 @@ export class BotProfileManager { } } + /** + * Build a nickname string that fits within TS3_NICKNAME_MAX. + * Uses UTF-8 byte length for the limit since TS3 counts bytes, + * not characters. + */ + private buildNickname(song: QueuedSong): string | null { + const songInfo = `${song.name} - ${song.artist}`; + const prefix = "\u266A "; // ♪ + const sep = " - "; + const suffix = `${sep}${this.defaultNickname}`; + + const overheadBytes = Buffer.byteLength(prefix, "utf8") + Buffer.byteLength(suffix, "utf8"); + if (overheadBytes >= TS3_NICKNAME_MAX) { + // Default nickname alone is too long with decoration — skip + return null; + } + + const maxSongBytes = TS3_NICKNAME_MAX - overheadBytes; + const truncated = this.truncateUtf8(songInfo, maxSongBytes); + return `${prefix}${truncated}${suffix}`; + } + + /** + * Truncate a string so its UTF-8 byte length does not exceed maxBytes. + * Appends an ellipsis if truncation occurred, taking its byte cost + * into account. Never splits a multi-byte character. + */ + private truncateUtf8(str: string, maxBytes: number): string { + if (Buffer.byteLength(str, "utf8") <= maxBytes) return str; + const ellipsis = "\u2026"; // … + const ellipsisBytes = Buffer.byteLength(ellipsis, "utf8"); // 3 + const target = maxBytes - ellipsisBytes; + if (target <= 0) return ellipsis; + // Walk characters, accumulating byte length + let byteLen = 0; + let end = 0; + for (const ch of str) { + const chBytes = Buffer.byteLength(ch, "utf8"); + if (byteLen + chBytes > target) break; + byteLen += chBytes; + end += ch.length; // ch.length handles surrogate pairs + } + return str.slice(0, end) + ellipsis; + } + private async updateChannelDescription(song: QueuedSong | null): Promise { if (!this.config.channelDescEnabled || this.permDenied.channelDesc) return; try { @@ -244,7 +303,6 @@ export class BotProfileManager { await this.tsClient.sendCommandNoWait( `channeledit cid=${channelId} channel_description=`, ); - this.logger.debug("Channel description cleared"); return; } @@ -257,7 +315,6 @@ export class BotProfileManager { await this.tsClient.sendCommandNoWait( `channeledit cid=${channelId} channel_description=${escapeTS3(desc)}`, ); - this.logger.debug("Channel description updated"); } catch (err) { this.handleFeatureError("channelDesc", err); } @@ -268,7 +325,6 @@ export class BotProfileManager { try { const text = `\u266A \u6B63\u5728\u64AD\u653E: ${song.name} - ${song.artist} [${song.album}]`; await this.tsClient.sendTextMessage(text); - this.logger.debug("Now-playing message sent"); } catch (err) { this.handleFeatureError("nowPlayingMsg", err); } @@ -278,18 +334,20 @@ export class BotProfileManager { /** * Append CDN resize parameters to get a thumbnail suitable for TS3 avatars. - * NetEase and QQ Music CDNs both support URL-based image resizing. + * NetEase and QQ Music CDNs support URL-based image resizing. + * BiliBili and YouTube covers fall through to the size-check guard. */ private thumbnailUrl(url: string): string { if (url.includes("music.126.net") || url.includes("netease")) { - // NetEase Cloud Music CDN: ?param=y return url.includes("?") ? url : `${url}?param=200y200`; } if (url.includes("qqmusic") || url.includes("qq.com")) { - // QQ Music CDN: /200 suffix or similar return url.replace(/\/\d+$/, "/200"); } - // Other platforms — return as-is, size check will skip if too large + if (url.includes("bilivideo") || url.includes("hdslb")) { + // BiliBili CDN supports @w_h suffix + return url.includes("@") ? url : `${url}@200w_200h`; + } return url; } @@ -307,6 +365,16 @@ export class BotProfileManager { } } + /** Race a promise against a timeout. */ + private withTimeout(promise: Promise, ms: number): Promise { + return Promise.race([ + promise, + new Promise((_, reject) => + setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms), + ), + ]); + } + private handleFeatureError( feature: keyof typeof this.permDenied, err: unknown, From e92628a6b471d9a7d5f0b10dd4051f3b2e97f009 Mon Sep 17 00:00:00 2001 From: saopig1 Date: Mon, 13 Apr 2026 00:48:09 +0800 Subject: [PATCH 10/11] fix(profile): add timeout to clientedit, guard clearAvatar with generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Wrap clientedit (description) with 5s timeout to prevent blocking channel description and now-playing updates if the command hangs - Check generation counter in clearAvatar to avoid clearing a newer song's avatar when stop→play happens in quick succession Co-Authored-By: Claude Opus 4.6 (1M context) --- src/bot/profile.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/bot/profile.ts b/src/bot/profile.ts index f8aeb48..8994fa8 100644 --- a/src/bot/profile.ts +++ b/src/bot/profile.ts @@ -114,7 +114,7 @@ export class BotProfileManager { if (!this.config.avatarEnabled || this.permDenied.avatar) return; try { if (!coverUrl) { - await this.clearAvatar(); + await this.clearAvatar(gen); return; } // Request a thumbnail from the CDN to stay within TS3's avatar size limit. @@ -152,7 +152,7 @@ export class BotProfileManager { await this.tsClient.sendCommandNoWait(`clientupdate client_flag_avatar=${escapeTS3(md5)}`); } - private async clearAvatar(): Promise { + private async clearAvatar(gen: number): Promise { try { await this.withTimeout( this.tsClient.fileTransferDeleteFile(0n, ["/avatar"]), @@ -161,6 +161,8 @@ export class BotProfileManager { } catch { // File may not exist or transfer timed out — that's fine } + // Bail if a newer song started while we were deleting + if (this.generation !== gen) return; try { await this.tsClient.sendCommandNoWait("clientupdate client_flag_avatar="); } catch (err) { @@ -183,8 +185,13 @@ export class BotProfileManager { // TS3AudioBot does via TSLib's ChangeDescription(). const clid = this.tsClient.getClientId(); if (clid <= 0) return; - await this.tsClient.execCommand( - `clientedit clid=${clid} client_description=${escapeTS3(text)}`, + // Use a 5s timeout — if clientedit hangs, don't block the + // remaining profile updates (channeledit, now-playing msg). + await this.withTimeout( + this.tsClient.execCommand( + `clientedit clid=${clid} client_description=${escapeTS3(text)}`, + ), + 5000, ); } this.logger.info("Description updated"); From a56dc5bc85a4eaa6ea9cfa053f3379ea9b5b31eb Mon Sep 17 00:00:00 2001 From: saopig1 Date: Mon, 13 Apr 2026 00:52:09 +0800 Subject: [PATCH 11/11] docs(readme): document bot profile auto-update feature Add profile feature to features list, architecture tree, and changelog. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d62856f..eb22110 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ - **B站视频音频提取** — 搜索B站视频,自动提取DASH最高码率音频流播放 - **B站热门推荐** — 首页展示B站热门视频和个性化推荐(登录后更准确) - **QR码登录** — 扫码登录网易云/QQ音乐/哔哩哔哩账号,Cookie 自动持久化 +- **机器人形象自动更新** — 播放时自动更新头像(专辑封面)、昵称(当前歌曲)、描述、Away 状态、频道描述,停止时恢复默认值。每项功能独立可配置,权限不足时自动降级 - **多机器人独立播放** — 多个机器人同时在不同服务器或频道播放不同音乐,每个机器人独立的播放队列、进度和音量,WebUI 一键切换控制 - **播放历史** — 自动记录所有播放过的歌曲 - **懒加载机制** — 歌单只存储元数据,播放时才获取链接(避免链接过期) @@ -289,7 +290,8 @@ teamspeak-music-bot/ │ ├── bot/ # 机器人核心 │ │ ├── commands.ts # 文字命令解析器(前缀、别名、权限) │ │ ├── instance.ts # Bot 实例(绑定 TS3 + 播放器 + 音源) -│ │ └── manager.ts # 多实例生命周期管理 +│ │ ├── manager.ts # 多实例生命周期管理 +│ │ └── profile.ts # 机器人形象管理(头像/昵称/描述/Away/频道描述) │ ├── data/ # 数据层 │ │ ├── config.ts # JSON 配置文件 │ │ └── database.ts # SQLite 数据库(播放历史、实例持久化) @@ -474,6 +476,15 @@ A:`git pull` 拉取最新代码,然后 `npm install && npm run build && npm ### 最新版本 +**机器人形象自动更新(Bot Profile)** + +- **播放时自动更新 TS 形象**:头像(专辑封面缩略图)、昵称(`♪ 歌名 - 歌手 - 原昵称`)、描述(歌曲信息)、Away 状态、频道描述、"正在播放"频道消息,全部随歌曲切换自动更新。 +- **停止播放时恢复默认**:头像清除、昵称恢复、Away 显示"等待播放"、描述和频道描述清空。 +- **权限安全**:每项功能独立检测权限,权限不足时自动禁用该功能(不影响其他功能和播放),重连后重试。 +- **独立可配置**:6 项功能可通过 REST API(`GET/PUT /api/player/:botId/profile`)独立开关,配置持久化到数据库。 +- **竞争条件防护**:generation 计数器防止快速切歌时旧头像覆盖新头像;UTF-8 字节长度截断中文昵称;文件传输操作带超时保护。 +- **TS3 适配**:描述通过 `clientedit`(非 `clientupdate`)设置,需要 `b_client_modify_description` 权限;昵称和 Away 通过合并的单条 `clientupdate` 避免命令队列超时。 + **协议层 & 稳定性** - **升级 `@honeybbq/teamspeak-client` 到 `0.2.1`**,移除内置 TS6 兼容层(`ts6-compat.ts`),改用库自带的通用 `clientinit` 协议(`3.?.? [Build: 5680278000]`),TS3/TS6 单一代码路径。