From 5e9ae49f526bf40849a29f1baf5b8dc0e099828c Mon Sep 17 00:00:00 2001 From: saopig1 <4x7sw862st@gmail.com> Date: Tue, 25 Aug 2026 17:59:31 +0800 Subject: [PATCH 1/2] fix(setup): stop a failed console write from aborting setup (#152) setup.bat runs `chcp 65001` and shows binary progress on stderr. On some Windows consoles - the reporter's Windows Server 2012 R2 above all - that code page cannot render non-ASCII text and the OS fails the write with EIO. process.stderr is an ordinary stream, so the EIO arrived as an 'error' event, and with no listener attached Node rethrew it as an uncaught exception: Error: write EIO { errno: -4070, code: 'EIO', syscall: 'write' } at log (scripts/download-binaries.mjs:84:18) at ensureFfmpeg (scripts/download-binaries.mjs:451:5) Those two frames pin it exactly: line 84 is `process.stderr.write`, and line 451 is the first log line of the whole run that contains Chinese. The three lines before it are pure ASCII and printed fine. Nothing was wrong with the download it was announcing - setup killed itself inside its own progress logging and reported the native modules as unusable. scripts/lib/console-log.mjs now wraps both streams: it listens for 'error' so the failure can never be fatal, then degrades that stream rather than dying - first to an ASCII rendering that keeps the English half of each bilingual line, then silent if the stream is really gone. The streams degrade independently, so a console that gives up costs setup.log nothing: that stdout is a redirected file. check-native.mjs gets the same treatment, since the console that cannot print its Chinese is exactly the one a user needs its English from. Also report a 404 honestly. better-sqlite3 dropped its Node 20 (ABI 115) prebuilds in 12.10.0 and @discordjs/opus 0.10.0 has none for Node 24, so users on those majors fall through to the source build and are told to install Python and a C++ toolchain - when switching Node major takes two minutes. Nothing in the output said so, and the README recommended Node 20 as if it still worked. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 10 +-- scripts/check-native.mjs | 7 +- scripts/download-binaries.mjs | 37 +++++++- scripts/lib/console-log.mjs | 142 +++++++++++++++++++++++++++++++ scripts/lib/console-log.test.mjs | 133 +++++++++++++++++++++++++++++ 5 files changed, 321 insertions(+), 8 deletions(-) create mode 100644 scripts/lib/console-log.mjs create mode 100644 scripts/lib/console-log.test.mjs diff --git a/README.md b/README.md index 0a335d7..dc66a32 100644 --- a/README.md +++ b/README.md @@ -65,22 +65,22 @@ 先装好 Node.js,其余依赖(含内置 FFmpeg)全部自动安装。 ``` -1. 安装 Node.js 20 LTS 或 22 LTS(https://nodejs.org/ 或 https://nodejs.cn/) +1. 安装 Node.js 22 LTS(https://nodejs.org/ 或 https://nodejs.cn/) 2. 下载或 clone 本项目 3. 双击 scripts\setup.bat (安装依赖并构建,不含 Node.js 本身) 4. 双击 scripts\start.bat (启动机器人) 5. 浏览器打开 http://localhost:3000 ``` -> **先装 Node.js 20 LTS 或 22 LTS**([nodejs.org](https://nodejs.org/) / 国内镜像 [nodejs.cn](https://nodejs.cn/))。`setup.bat` 检测到没装 Node 时会给出下载地址并退出,不会替你安装。 +> **先装 Node.js 22 LTS**([nodejs.org](https://nodejs.org/) / 国内镜像 [nodejs.cn](https://nodejs.cn/))。`setup.bat` 检测到没装 Node 时会给出下载地址并退出,不会替你安装。 > > 之后 `setup.bat` 会运行 `npm install` 安装所有依赖(包括内置 FFmpeg),按当前 Node 版本准备好原生模块,最后构建项目。之后每次只需双击 `start.bat` 启动。 > -> 更新的 Node 大版本(如 24)也能用,但通常没有现成的 opus / better-sqlite3 预编译包,安装脚本会改用源码编译,需要 C/C++ 构建工具且耗时更久——所以推荐 20 / 22 LTS。**装好之后不要再换 Node 大版本**:原生模块只能在编译它的那个版本上加载,换版本后必须重新运行 `setup.bat`(脚本会自动检测并重装,见下方常见问题)。 +> 其他 Node 大版本也能用,但通常没有现成的预编译包,安装脚本会改用源码编译,需要 Python + C/C++ 构建工具且耗时更久:better-sqlite3 从 12.10.0 起不再提供 Node 20(ABI 115)的预编译包,@discordjs/opus 0.10.0 也没有 Node 24(ABI 137)的——所以推荐 22 LTS。**装好之后不要再换 Node 大版本**:原生模块只能在编译它的那个版本上加载,换版本后必须重新运行 `setup.bat`(脚本会自动检测并重装,见下方常见问题)。 ### 方式二:手动安装(所有系统) -**前置条件:** [Node.js 20 LTS 或 22 LTS](https://nodejs.org/)(推荐;更新的大版本可用但需要源码编译原生模块)和一个 TeamSpeak 服务器(TS3/TS5/TS6 均可)。 +**前置条件:** [Node.js 22 LTS](https://nodejs.org/)(推荐;Node 20 / 24 等其他大版本可用,但需要源码编译原生模块)和一个 TeamSpeak 服务器(TS3/TS5/TS6 均可)。 FFmpeg **已自动内置**,无需手动安装。 ```bash @@ -510,7 +510,7 @@ teamspeak-music-bot/ | 层级 | 技术 | |------|------| -| **运行时** | Node.js 20 / 22 LTS, TypeScript 5 | +| **运行时** | Node.js 22 LTS(推荐), TypeScript 5 | | **后端框架** | Express 4, WebSocket (ws) | | **数据库** | better-sqlite3 (SQLite) | | **音频处理** | FFmpeg (ffmpeg-static 内置), @discordjs/opus | diff --git a/scripts/check-native.mjs b/scripts/check-native.mjs index 4393628..e63e403 100644 --- a/scripts/check-native.mjs +++ b/scripts/check-native.mjs @@ -23,6 +23,7 @@ import { execFileSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +import { createLineWriter } from "./lib/console-log.mjs"; const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); const NODE_MODULES = join(ROOT, "node_modules"); @@ -106,7 +107,11 @@ for (const spec of REQUIRED) { function report() { const stamp = readStamp(); const mismatch = broken.find((b) => b.abiMismatch); - const out = (line) => process.stderr.write(`${line}\n`); + // Never let a failed console write become an uncaught error and replace this + // report with a stack trace - the console that cannot print the Chinese half + // of these lines is exactly the one a user needs the English half from. + // See scripts/lib/console-log.mjs and issue #152. + const out = createLineWriter(process.stderr); out(""); out("============================================================"); diff --git a/scripts/download-binaries.mjs b/scripts/download-binaries.mjs index 2403f14..df6b68d 100755 --- a/scripts/download-binaries.mjs +++ b/scripts/download-binaries.mjs @@ -51,6 +51,7 @@ import { Readable } from "node:stream"; import { execFileSync, execSync } from "node:child_process"; import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; +import { createLineWriter } from "./lib/console-log.mjs"; const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); const NODE_MODULES = join(ROOT, "node_modules"); @@ -61,6 +62,11 @@ const CDN = process.argv[2] || "https://cdn.npmmirror.com/binaries"; const PLATFORM = process.platform; const ARCH = process.arch; const NODE_ABI = process.versions.modules; +const NODE_MAJOR = Number(process.versions.node.split(".")[0]); +/** The newest Node major this project is regularly tested against, and the one + * every required addon currently ships a prebuild for. Keep in sync with + * TESTED_NODE_MAJOR in scripts/setup.bat. */ +const TESTED_NODE_MAJOR = 22; /** Modules the bot cannot start without. ffmpeg-static is optional: a system * ffmpeg on PATH is a documented fallback, so it only ever produces a WARN. */ @@ -79,10 +85,17 @@ const FFMPEG_MIN_BYTES = 20 * 1024 * 1024; // log keeps the full transcript too. const ECHO_STDOUT = process.env.TSMB_BINARY_LOG_STDOUT === "1"; +// Both writers swallow a failed write instead of letting it become an uncaught +// 'error' event: a console that cannot print the Chinese half of a line (issue +// #152) must not be able to abort a whole setup run. The two streams degrade +// independently, so setup.log keeps the full bilingual transcript either way. +const writeErr = createLineWriter(process.stderr); +const writeOut = createLineWriter(process.stdout); + function log(msg) { const line = msg === "" ? "" : ` [binary] ${msg}`; - process.stderr.write(`${line}\n`); - if (ECHO_STDOUT) process.stdout.write(`${line}\n`); + writeErr(line); + if (ECHO_STDOUT) writeOut(line); } // --------------------------------------------------------------------------- @@ -401,6 +414,24 @@ function buildFromSource(command) { execSync(command, { cwd: ROOT, stdio: ["ignore", "inherit", "inherit"] }); } +/** + * A 404 from the CDN is not a mirror outage: it means this exact package + * version publishes no prebuilt binary for the running Node ABI at all. + * better-sqlite3 dropped its Node 20 (ABI 115) builds in 12.10.0, and + * @discordjs/opus 0.10.0 has none for Node 24 (ABI 137) - so a user on either + * of those majors lands in the source-build fallback below and is told to + * install Python and a C++ toolchain. Switching Node major is the far cheaper + * fix, and nothing else in this output points at it. See issue #152. + */ +function explainMissingPrebuild(name, version, err) { + if (!/HTTP 404/.test(err.message)) return; + log(`${name}: ${name}@${version} ships no prebuilt binary for Node ${NODE_MAJOR} (ABI ${NODE_ABI})`); + log( + `${name}: Node ${TESTED_NODE_MAJOR} LTS has one — switching Node is usually much quicker than ` + + `setting up a compiler (换用 Node ${TESTED_NODE_MAJOR} LTS 通常比装编译环境快得多)`, + ); +} + function buildToolsHint() { log("Install build tools first:"); log(" Windows: npm install --global windows-build-tools (或安装 Visual Studio Build Tools + Python)"); @@ -521,6 +552,7 @@ async function ensureOpus() { log(`${name}: prebuilt binary installed`); } catch (cdnErr) { log(`${name}: CDN install failed (${cdnErr.message})`); + explainMissingPrebuild(name, version, cdnErr); log(`${name}: falling back to a source build — 'npm rebuild ${name}' (可能需要几分钟)`); buildFromSource(`npm rebuild ${name}`); } @@ -591,6 +623,7 @@ async function ensureBetterSqlite3() { log(`${name}: prebuilt binary installed (${humanSize(dest)})`); } catch (cdnErr) { log(`${name}: CDN install failed (${cdnErr.message})`); + explainMissingPrebuild(name, version, cdnErr); log(`${name}: falling back to a source build — 'npm rebuild ${name} --build-from-source' (可能需要几分钟)`); buildFromSource(`npm rebuild ${name} --build-from-source`); } diff --git a/scripts/lib/console-log.mjs b/scripts/lib/console-log.mjs new file mode 100644 index 0000000..0c397f6 --- /dev/null +++ b/scripts/lib/console-log.mjs @@ -0,0 +1,142 @@ +/** + * Crash-proof line logging for the setup scripts. + * + * WHY THIS EXISTS (issue #152) + * ---------------------------- + * setup.bat runs `chcp 65001` and shows progress on stderr. Some Windows + * consoles - Windows Server 2012 R2 above all - cannot render non-ASCII text in + * that code page and the OS fails the write with EIO. `process.stderr` is an + * ordinary stream, so that EIO arrives as an 'error' event, and a stream with + * no 'error' listener rethrows it as an uncaught exception: + * + * Error: write EIO + * at afterWriteDispatched (node:internal/stream_base_commons:159:15) + * ... + * at log (scripts/download-binaries.mjs:84:18) + * at ensureFfmpeg (scripts/download-binaries.mjs:451:5) + * + * That is setup killing itself inside its own progress logging, on the first + * line of the run that happened to contain Chinese - nothing was wrong with the + * download it was about to start. + * + * So: listen for the error and degrade instead of dying. + * full -> ascii : drop the CJK the console choked on, keep the English half + * ascii -> off : the stream is simply gone (closed pipe) - stay quiet + * Each stream degrades on its own, so a console that gives up does not cost + * setup.log its full bilingual transcript: that stdout is a redirected file. + */ + +const HAS_NON_ASCII = /[^\x00-\x7F]/; + +/** Placeholders for a removed run: one that separated words, one that did not. */ +const SPACED = "\u0000"; +const TIGHT = "\u0001"; + +/** Punctuation the bilingual strings use that has an obvious ASCII twin. */ +const PUNCTUATION = new Map( + Object.entries({ + "—": "-", + "–": "-", + "…": "...", + "“": '"', + "”": '"', + "‘": "'", + "’": "'", + ",": ",", + "。": ".", + "、": ",", + ":": ":", + ";": ";", + "(": "(", + ")": ")", + "!": "!", + "?": "?", + "←": "<-", + "→": "->", + "×": "x", + }), +); + +/** + * Best-effort ASCII rendering of a log line, for a console that cannot print + * anything else. Returns null when nothing worth printing survives - every + * Chinese-only line in these scripts sits directly beside an English line + * saying the same thing, so dropping it loses no information. + */ +export function toAsciiFallback(text) { + if (!HAS_NON_ASCII.test(text)) return text; + + let out = ""; + for (const ch of text) out += PUNCTUATION.get(ch) ?? ch; + + out = out + .replace(/[\u0000\u0001]/g, "") + // Mark each removed run rather than just deleting it, so the tidy-up below + // can tell "a separator that introduced text we dropped" from "a separator + // that belongs to the English half". SPACED was holding two ASCII words + // apart; TIGHT was hugging a bracket or a comma. + .replace(/[ \t]*[^\x00-\x7F]+[ \t]*/g, (run) => + /^[ \t]/.test(run) && /[ \t]$/.test(run) ? SPACED : TIGHT, + ) + // "(可能需要几分钟)" — the parentheses held nothing else. + .replace(/[ \t]*\([ \t]*(?:[\u0000\u0001][ \t]*)+\)/g, "") + // "FAILED — 编译失败", "(~80 MB, 请耐心等待)" — drop the trailing marks along + // with the separators that were only ever there to introduce them. + .replace(/[ \t]*[-,;:]*[ \t]*(?:[\u0000\u0001][ \t,;:-]*)+(?=[)\]]|$)/gm, "") + .replace(/\u0000/g, " ") + .replace(/\u0001/g, "") + .replace(/[ \t]+$/gm, ""); + + return /[A-Za-z0-9]/.test(out) ? out : null; +} + +/** One degradation state per stream, shared by every writer built on it. */ +const guards = new WeakMap(); + +function guardFor(stream) { + const existing = guards.get(stream); + if (existing) return existing; + + const guard = { mode: "full" }; + guards.set(stream, guard); + try { + // The whole point: without this listener the next EIO/EPIPE is fatal. + stream.on("error", () => degrade(guard)); + } catch { + /* not an EventEmitter - the try/catch around write() still guards us */ + } + return guard; +} + +function degrade(guard) { + guard.mode = guard.mode === "full" ? "ascii" : "off"; +} + +/** + * Build a `writeLine(text)` that appends a newline, never throws, and never + * lets a failed console write take the process down with it. + * Returns true when the line reached the stream. + */ +export function createLineWriter(stream) { + const guard = guardFor(stream); + + return function writeLine(text) { + if (guard.mode === "off") return false; + + let line = text; + if (guard.mode === "ascii") { + line = toAsciiFallback(text); + if (line === null) return false; + } + + try { + stream.write(`${line}\n`); + return true; + } catch { + // A synchronous throw (EBADF on a closed handle) never reaches the + // 'error' listener, so degrade here too. + degrade(guard); + return false; + } + }; +} diff --git a/scripts/lib/console-log.test.mjs b/scripts/lib/console-log.test.mjs new file mode 100644 index 0000000..2e44491 --- /dev/null +++ b/scripts/lib/console-log.test.mjs @@ -0,0 +1,133 @@ +import { EventEmitter } from "node:events"; +import { describe, expect, it, vi } from "vitest"; + +import { createLineWriter, toAsciiFallback } from "./console-log.mjs"; + +/** Stand-in for process.stderr: an EventEmitter with a write() we can steer. */ +function fakeStream() { + const stream = new EventEmitter(); + stream.written = []; + stream.throwOnWrite = false; + stream.write = (chunk) => { + if (stream.throwOnWrite) throw new Error("EBADF"); + stream.written.push(chunk); + return true; + }; + return stream; +} + +describe("toAsciiFallback", () => { + it("leaves ASCII lines exactly as they are", () => { + const line = " [binary] better-sqlite3: OK (loads under v22.23.2, ABI 127)"; + expect(toAsciiFallback(line)).toBe(line); + expect(toAsciiFallback("")).toBe(""); + }); + + it("keeps the English half of the line that crashed setup in #152", () => { + expect( + toAsciiFallback( + " [binary] ffmpeg-static: GET https://cdn/ffmpeg.gz (~80 MB, 这一步比较慢,请耐心等待)", + ), + ).toBe(" [binary] ffmpeg-static: GET https://cdn/ffmpeg.gz (~80 MB)"); + }); + + it("drops parentheses and separators left stranded by the removed text", () => { + expect( + toAsciiFallback(" [binary] better-sqlite3: falling back — 'npm rebuild' (可能需要几分钟)"), + ).toBe(" [binary] better-sqlite3: falling back - 'npm rebuild'"); + expect( + toAsciiFallback(" Windows: npm install --global windows-build-tools (或安装 VS Build Tools)"), + ).toBe(" Windows: npm install --global windows-build-tools (VS Build Tools)"); + }); + + it("drops a Chinese-only line, which always has an English twin beside it", () => { + expect(toAsciiFallback("必需的原生模块不可用,机器人无法启动 —— 请查看上面的错误信息。")).toBeNull(); + }); + + it("preserves the indentation the summary is aligned on, and drops the dangling dash", () => { + expect(toAsciiFallback(" - better-sqlite3 FAILED — 编译失败")).toBe( + " - better-sqlite3 FAILED", + ); + }); + + it("keeps a separator that belongs to the English half", () => { + expect(toAsciiFallback("Summary — Node v22.0.0 / ABI 127 / win32-x64:")).toBe( + "Summary - Node v22.0.0 / ABI 127 / win32-x64:", + ); + }); +}); + +describe("createLineWriter", () => { + it("appends a newline and reports the write", () => { + const stream = fakeStream(); + expect(createLineWriter(stream)("hello")).toBe(true); + expect(stream.written).toEqual(["hello\n"]); + }); + + it("survives the EIO that killed setup: an 'error' event must not throw", () => { + const stream = fakeStream(); + createLineWriter(stream); + expect(stream.listenerCount("error")).toBe(1); + expect(() => stream.emit("error", Object.assign(new Error("write EIO"), { code: "EIO" }))).not.toThrow(); + }); + + it("falls back to ASCII once the console has refused a line", () => { + const stream = fakeStream(); + const write = createLineWriter(stream); + write(" [binary] GET https://cdn/ffmpeg.gz (~80 MB, 这一步比较慢,请耐心等待)"); + stream.emit("error", new Error("write EIO")); + write(" [binary] GET https://cdn/opus.tar.gz (~1 MB, 这一步比较慢,请耐心等待)"); + expect(stream.written).toEqual([ + " [binary] GET https://cdn/ffmpeg.gz (~80 MB, 这一步比较慢,请耐心等待)\n", + " [binary] GET https://cdn/opus.tar.gz (~1 MB)\n", + ]); + }); + + it("goes quiet after a second failure rather than retrying a dead stream", () => { + const stream = fakeStream(); + const write = createLineWriter(stream); + stream.emit("error", new Error("write EIO")); + stream.emit("error", new Error("write EPIPE")); + expect(write("anything at all")).toBe(false); + expect(stream.written).toEqual([]); + }); + + it("degrades on a synchronous throw too, which never reaches the listener", () => { + const stream = fakeStream(); + const write = createLineWriter(stream); + stream.throwOnWrite = true; + expect(write(" [binary] 下载中 downloading")).toBe(false); + stream.throwOnWrite = false; + write(" [binary] 下载中 downloading"); + expect(stream.written).toEqual([" [binary] downloading\n"]); + }); + + it("degrades each stream on its own, so setup.log keeps the full transcript", () => { + const console_ = fakeStream(); + const logFile = fakeStream(); + const writeConsole = createLineWriter(console_); + const writeLog = createLineWriter(logFile); + console_.emit("error", new Error("write EIO")); + + const line = " [binary] ffmpeg-static: 下载完成 done"; + writeConsole(line); + writeLog(line); + + expect(console_.written).toEqual([" [binary] ffmpeg-static: done\n"]); + expect(logFile.written).toEqual([`${line}\n`]); + }); + + it("never installs a second listener for a stream that already has a writer", () => { + const stream = fakeStream(); + createLineWriter(stream); + createLineWriter(stream); + expect(stream.listenerCount("error")).toBe(1); + }); + + it("still guards a stream that is not an EventEmitter", () => { + const stream = { write: vi.fn(() => { throw new Error("EBADF"); }) }; + const write = createLineWriter(stream); + expect(() => write("line")).not.toThrow(); + expect(write("line")).toBe(false); + }); +}); From a804b2edc198744b103b30d2df799878c03e2347 Mon Sep 17 00:00:00 2001 From: saopig1 <4x7sw862st@gmail.com> Date: Tue, 25 Aug 2026 18:27:22 +0800 Subject: [PATCH 2/2] feat(setup)!: require Node 22.12+ and drop Node 20 (#152) better-sqlite3 stopped publishing prebuilt binaries for Node 20's ABI (115) in 12.10.0 - upstream, not a mirror gap: 12.8.0 / 12.9.0 115 127 131 137 141 12.10.0+ 127 137 141 147 `better-sqlite3: ^12.8.0` resolves well past that, so every Node 20 install 404'd on the CDN, fell through to the source build, and demanded Python plus a C++ toolchain before the bot could start at all. package.json went on claiming `^20.19.0` worked, and the README went on recommending Node 20 as one of two blessed versions. It was not a supported configuration in any meaningful sense - it was a trap. So say so up front: engines, both setup scripts, and the Docker images now require Node 22.12+ (or 24+, which still needs a source build for opus). The version gate in setup.bat / setup.sh is kept byte-identical to the engines range, as before. Also copy scripts/lib/console-log.mjs into the production image. The previous commit had check-native.mjs import it, and the Dockerfile copies check-native.mjs in on its own for `docker exec ... npm start` - without its dependency that preflight now dies with ERR_MODULE_NOT_FOUND. BREAKING CHANGE: Node 20 is no longer supported. Node 22.12 LTS or newer is required; setup refuses to run on anything older. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 4 ++-- package.json | 2 +- scripts/docker/Dockerfile | 6 ++++-- scripts/download-binaries.mjs | 18 +++++++++--------- scripts/setup.bat | 20 +++++++++++--------- scripts/setup.sh | 14 ++++++++------ 6 files changed, 35 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index dc66a32..e58cd4b 100644 --- a/README.md +++ b/README.md @@ -76,11 +76,11 @@ > > 之后 `setup.bat` 会运行 `npm install` 安装所有依赖(包括内置 FFmpeg),按当前 Node 版本准备好原生模块,最后构建项目。之后每次只需双击 `start.bat` 启动。 > -> 其他 Node 大版本也能用,但通常没有现成的预编译包,安装脚本会改用源码编译,需要 Python + C/C++ 构建工具且耗时更久:better-sqlite3 从 12.10.0 起不再提供 Node 20(ABI 115)的预编译包,@discordjs/opus 0.10.0 也没有 Node 24(ABI 137)的——所以推荐 22 LTS。**装好之后不要再换 Node 大版本**:原生模块只能在编译它的那个版本上加载,换版本后必须重新运行 `setup.bat`(脚本会自动检测并重装,见下方常见问题)。 +> **Node 20 已不再支持**:better-sqlite3 从 12.10.0 起不再发布它那个 ABI(115)的预编译包,装起来必须先备好 Python + C++ 构建工具([#152](https://github.com/ZHANGTIANYAO1/teamspeak-music-bot/issues/152))。Node 24 及更新的大版本能用,但 @discordjs/opus 0.10.0 同样没有 Node 24(ABI 137)的预编译包,安装脚本会改用源码编译,需要构建工具且耗时更久——所以推荐 22 LTS。**装好之后不要再换 Node 大版本**:原生模块只能在编译它的那个版本上加载,换版本后必须重新运行 `setup.bat`(脚本会自动检测并重装,见下方常见问题)。 ### 方式二:手动安装(所有系统) -**前置条件:** [Node.js 22 LTS](https://nodejs.org/)(推荐;Node 20 / 24 等其他大版本可用,但需要源码编译原生模块)和一个 TeamSpeak 服务器(TS3/TS5/TS6 均可)。 +**前置条件:** [Node.js 22 LTS](https://nodejs.org/)(Node 24 及更新版本也能用,但需要源码编译原生模块;Node 20 已不再支持)和一个 TeamSpeak 服务器(TS3/TS5/TS6 均可)。 FFmpeg **已自动内置**,无需手动安装。 ```bash diff --git a/package.json b/package.json index 1cdd85a..0c52047 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "TeamSpeak music bot with NetEase Cloud Music and QQ Music support", "type": "module", "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.12.0 || >=24.0.0" }, "scripts": { "dev": "tsx watch src/index.ts", diff --git a/scripts/docker/Dockerfile b/scripts/docker/Dockerfile index 735bb33..84fe8d9 100644 --- a/scripts/docker/Dockerfile +++ b/scripts/docker/Dockerfile @@ -4,7 +4,7 @@ # ========================================== # --- Stage 1: Build backend + frontend --- -FROM node:20-slim AS builder +FROM node:22-slim AS builder # Install build tools for native modules (opus, better-sqlite3) RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -30,7 +30,7 @@ RUN npm run build RUN rm -rf node_modules && npm ci --production && npm cache clean --force # --- Stage 2: Production image --- -FROM node:20-slim +FROM node:22-slim # Install system FFmpeg — the ffmpeg-static npm package bundles a pre-compiled # binary that can SIGSEGV inside Docker (incompatible glibc / missing libs). @@ -51,6 +51,8 @@ COPY --from=builder /app/node_modules ./node_modules # goes through npm, but an interactive `docker exec ... npm start` would # otherwise die on a missing script rather than starting the bot. COPY --from=builder /app/scripts/check-native.mjs ./scripts/check-native.mjs +# ...and the module it imports for crash-proof logging. +COPY --from=builder /app/scripts/lib/console-log.mjs ./scripts/lib/console-log.mjs # Data directory for database, cookies, logs RUN mkdir -p /app/data diff --git a/scripts/download-binaries.mjs b/scripts/download-binaries.mjs index df6b68d..616b3be 100755 --- a/scripts/download-binaries.mjs +++ b/scripts/download-binaries.mjs @@ -417,11 +417,12 @@ function buildFromSource(command) { /** * A 404 from the CDN is not a mirror outage: it means this exact package * version publishes no prebuilt binary for the running Node ABI at all. - * better-sqlite3 dropped its Node 20 (ABI 115) builds in 12.10.0, and - * @discordjs/opus 0.10.0 has none for Node 24 (ABI 137) - so a user on either - * of those majors lands in the source-build fallback below and is told to - * install Python and a C++ toolchain. Switching Node major is the far cheaper - * fix, and nothing else in this output points at it. See issue #152. + * @discordjs/opus 0.10.0 has no build for Node 24 (ABI 137), so a user on that + * major lands in the source-build fallback below and is told to install Python + * and a C++ toolchain. Switching Node major is the far cheaper fix, and nothing + * else in this output points at it. (better-sqlite3 dropping its Node 20 / ABI + * 115 builds in 12.10.0 is why Node 20 is no longer accepted at all.) + * See issue #152. */ function explainMissingPrebuild(name, version, err) { if (!/HTTP 404/.test(err.message)) return; @@ -694,10 +695,9 @@ try { // that same loop. Run these concurrently and the first module to fall back to // a source build kills every download still in flight — the connection is // healthy, the timer just never got a chance to be reset. That is not a rare - // race: npmmirror has no opus prebuild for ABI 137 (Node 24) and no - // better-sqlite3 prebuild for ABI 115 (Node 20), so on both of the Node - // versions this project supports, one module 404s within ~100ms and starts - // building while ffmpeg's ~80MB download is still going. ffmpeg is optional, + // race: @discordjs/opus 0.10.0 has no prebuild for ABI 137, so on Node 24 that + // module 404s within ~100ms and starts building while ffmpeg's ~80MB download + // is still going. ffmpeg is optional, // so the spurious failure used to be swallowed as a WARN and setup still // reported success — leaving the user with no ffmpeg and no working playback. // Nothing here benefits from overlap anyway: every probe is execFileSync. diff --git a/scripts/setup.bat b/scripts/setup.bat index e1fb536..f05b3f9 100644 --- a/scripts/setup.bat +++ b/scripts/setup.bat @@ -11,7 +11,7 @@ title TSMusicBot Setup :: ============================================================ set "SCRIPT_VERSION=2.2" -set "MIN_NODE_MAJOR=20" +set "MIN_NODE_MAJOR=22" :: Newest Node major this project is regularly tested against. Anything above :: still works, it just may have no prebuilt addons and fall back to a source build. set "TESTED_NODE_MAJOR=22" @@ -67,13 +67,15 @@ for /f "tokens=1 delims=v." %%a in ("%NODE_VER%") do set "NODE_MAJOR=%%a" call :log "Node.js version: %NODE_VER%" echo [OK] Node.js found: %NODE_VER% -:: The supported floor is not just a major version, so let node decide: -:: @honeybbq/teamspeak-client needs >=20.19, @sansenjian/qq-music-api needs -:: >=20.17 / >=22.9, and the odd majors (21 / 23) are excluded by -:: better-sqlite3 and vitest. Keep this in sync with "engines" in package.json. -node -e "const v=process.versions.node.split('.').map(Number); process.exit((v[0]===20&&v[1]>=19)||(v[0]===22&&v[1]>=12)||v[0]>=24?0:1)" +:: The supported floor is not just a major version, so let node decide. +:: Node 20 was dropped: better-sqlite3 ships no prebuilt binary for its ABI +:: (115) since 12.10.0, so every Node 20 install needed Python and a C++ +:: toolchain just to get off the ground (issue #152). The odd majors (21 / +:: 23) are excluded by better-sqlite3 and vitest. +:: Keep this in sync with "engines" in package.json. +node -e "const v=process.versions.node.split('.').map(Number); process.exit((v[0]===22&&v[1]>=12)||v[0]>=24?0:1)" if errorlevel 1 ( - call :error "Node.js %NODE_VER% is not supported. Use Node 20.19+ LTS or Node 22.12+ LTS." + call :error "Node.js %NODE_VER% is not supported. Use Node 22.12+ LTS or newer." echo Download: https://nodejs.org/ or https://nodejs.cn/ pause exit /b 1 @@ -86,10 +88,10 @@ if errorlevel 1 ( :: characters and starts eating the "echo " prefix of following lines. :: Bilingual guidance lives in the Node scripts, which print UTF-8 reliably. if %NODE_MAJOR% GTR %TESTED_NODE_MAJOR% ( - echo [WARN] Node %NODE_VER% is newer than the tested LTS line, Node 20 / Node 22. + echo [WARN] Node %NODE_VER% is newer than the tested LTS line, Node 22. echo Newer Node majors may have no prebuilt opus / better-sqlite3, echo so setup falls back to a source build - slower, needs C++ build tools. - echo Recommended: Node 20 LTS or Node 22 LTS - https://nodejs.org/ or https://nodejs.cn/ + echo Recommended: Node 22 LTS - https://nodejs.org/ or https://nodejs.cn/ echo This is only a warning; setup still builds the binaries for %NODE_VER%. call :log "[WARN] Node major %NODE_MAJOR% is newer than tested LTS %TESTED_NODE_MAJOR%" ) diff --git a/scripts/setup.sh b/scripts/setup.sh index d54bf44..154b2f4 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -32,16 +32,18 @@ echo "[OK] Node.js $(node -v)" TESTED_NODE_MAJOR=22 NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')" -# The floor is not just a major version, so let node decide: @honeybbq/teamspeak-client -# needs >=20.19, @sansenjian/qq-music-api needs >=20.17 / >=22.9, and the odd majors -# (21 / 23) are excluded by better-sqlite3 and vitest. Keep in sync with package.json "engines". -if ! node -e 'const v=process.versions.node.split(".").map(Number); process.exit((v[0]===20&&v[1]>=19)||(v[0]===22&&v[1]>=12)||v[0]>=24?0:1)'; then - echo "[ERROR] Node.js $(node -v) is not supported. Use Node 20.19+ LTS or Node 22.12+ LTS." +# The floor is not just a major version, so let node decide. Node 20 was dropped: +# better-sqlite3 ships no prebuilt binary for its ABI (115) since 12.10.0, so every +# Node 20 install needed Python and a C++ toolchain just to get off the ground +# (issue #152). The odd majors (21 / 23) are excluded by better-sqlite3 and vitest. +# Keep in sync with package.json "engines". +if ! node -e 'const v=process.versions.node.split(".").map(Number); process.exit((v[0]===22&&v[1]>=12)||v[0]>=24?0:1)'; then + echo "[ERROR] Node.js $(node -v) is not supported. Use Node 22.12+ LTS or newer." echo " https://nodejs.org/ | https://nodejs.cn/" exit 1 fi if [ "$NODE_MAJOR" -gt "$TESTED_NODE_MAJOR" ]; then - echo "[WARN] Node $(node -v) is newer than the tested LTS line (Node 20 / Node 22)." + echo "[WARN] Node $(node -v) is newer than the tested LTS line (Node 22)." echo " 新版 Node 可能没有现成的 opus / better-sqlite3 预编译包," echo " 安装时会自动改用源码编译,需要 C/C++ 构建工具,速度较慢。" echo " This is only a warning - setup builds the binaries for $(node -v) either way."