chore(deps): bump @honeybbq/teamspeak-client to 0.2.1, drop ts6-compat shim

Version 0.2.1 ships a universal clientinit format that works natively
against both TS3 and TS6 servers:

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

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

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

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.6 committed 2026-04-11 01:54:43 +08:00
1 parent 4643f70f4a
commit ca4cb1790a
5 files changed
+11 -111

No files matched your search

+4 -4
View File
@@ -10,7 +10,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@discordjs/opus": "^0.10.0", "@discordjs/opus": "^0.10.0",
"@honeybbq/teamspeak-client": "^0.1.0", "@honeybbq/teamspeak-client": "^0.2.1",
"@koa/router": "^15.4.0", "@koa/router": "^15.4.0",
"@sansenjian/qq-music-api": "^2.2.9", "@sansenjian/qq-music-api": "^2.2.9",
"axios": "^1.14.0", "axios": "^1.14.0",
@@ -573,9 +573,9 @@
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/@honeybbq/teamspeak-client": { "node_modules/@honeybbq/teamspeak-client": {
"version": "0.1.0", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/@honeybbq/teamspeak-client/-/teamspeak-client-0.1.0.tgz", "resolved": "https://registry.npmjs.org/@honeybbq/teamspeak-client/-/teamspeak-client-0.2.1.tgz",
"integrity": "sha512-Gh/siE4a0NRxqEqiwbvYn6p4ulphG6IVZaTCuCUjS/+cD6rVvWNrCgbJ0724RdkEY5yndoQ34zkjE/Lglk1wTQ==", "integrity": "sha512-iLZRWsqeAA/IuEL0zMAhHm9+wQ1YmByXjh9T6PZLYSxFk56T39MpQobT6eohM/PnIz3wQZymd8h701hyliLo2w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@noble/curves": "^2.0.1" "@noble/curves": "^2.0.1"
+1 -1
View File
@@ -15,7 +15,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@discordjs/opus": "^0.10.0", "@discordjs/opus": "^0.10.0",
"@honeybbq/teamspeak-client": "^0.1.0", "@honeybbq/teamspeak-client": "^0.2.1",
"@koa/router": "^15.4.0", "@koa/router": "^15.4.0",
"@sansenjian/qq-music-api": "^2.2.9", "@sansenjian/qq-music-api": "^2.2.9",
"axios": "^1.14.0", "axios": "^1.14.0",
+6 -32
View File
@@ -17,7 +17,6 @@ import {
type ServerProtocol, type ServerProtocol,
} from "./protocol-detect.js"; } from "./protocol-detect.js";
import { TS6HttpQuery } from "./http-query.js"; import { TS6HttpQuery } from "./http-query.js";
import { patchClientInitVersion } from "./ts6-compat.js";
export { CODEC_OPUS_MUSIC } from "./voice.js"; export { CODEC_OPUS_MUSIC } from "./voice.js";
export type { ServerProtocol } from "./protocol-detect.js"; export type { ServerProtocol } from "./protocol-detect.js";
@@ -206,37 +205,12 @@ export class TS3Client extends EventEmitter {
}); });
await this.client.connect(); await this.client.connect();
// Note: @honeybbq/teamspeak-client 0.2.x ships a universal clientinit
// Patch handler.sendPacket to intercept clientinit during handshake for TS6. // (client_version "3.?.? [Build: 5680278000]" + matching signature)
// // that works against both TS3 and TS6 servers. The old 3.6.2 monkey-
// Why after connect(): The library's connect() internally calls #S() which // patch on handler.sendPacket was removed when we bumped to 0.2.1 — it
// replaces this.handler with a fresh PacketHandler. Any patch applied before // would have replaced the library's new correct version with a stale
// connect() would be discarded. Patching here is safe because connect() only // signature and made TS6 handshakes fail.
// establishes the UDP socket and sends the first Init1 packet — the actual
// clientinit command is sent later in async message callbacks (after Init1
// round-trips complete), which cannot fire before this sync continuation.
//
// Why not commandMiddleware: The library's middleware chain only applies to
// post-connection commands (sendCommandNoWait/execCommand). The handshake's
// clientinit is sent directly via handler.sendPacket(), bypassing middleware.
if (this.detectedProtocol === "ts6") {
this.logger.info("Applying TS6 compatibility: upgrading client_version to 3.6.2");
const handler = this.client.handler;
const origSendPacket = handler.sendPacket.bind(handler);
handler.sendPacket = (pType: number, data: Uint8Array, flags: number) => {
// PacketType.Command = 2
if (pType === 2) {
const str = Buffer.from(data).toString("utf-8");
if (str.startsWith("clientinit ")) {
const patched = patchClientInitVersion(str);
origSendPacket(pType, Buffer.from(patched), flags);
return;
}
}
origSendPacket(pType, data, flags);
};
}
await this.client.waitConnected(); await this.client.waitConnected();
this.clientId = this.client.clientID(); this.clientId = this.client.clientID();
this.voiceFramesSent = 0; this.voiceFramesSent = 0;
-22
View File
@@ -1,22 +0,0 @@
import { describe, it, expect } from "vitest";
import { patchClientInitVersion } from "./ts6-compat.js";
describe("ts6-compat", () => {
it("patches clientinit version to 3.6.2", () => {
const original =
"clientinit client_nickname=Bot client_version=3.5.3\\s[Build:\\s1587971024] " +
"client_platform=Windows client_version_sign=Kvmj7qX6wJCPI5GVT71samfmhz/bvs7M+OTXWB/JWxdQbxDe17xda7dzUWLX7pjvdJTqZmbse1HBmTxThPKvAg== " +
"client_key_offset=42 hwid=abc123";
const patched = patchClientInitVersion(original);
expect(patched).toContain("client_version=3.6.2\\s[Build:\\s1695203293]");
expect(patched).toContain(
"client_version_sign=4OH3unxjGNBYS5EN4RFNrEo3UJz2Jn5KW1JqMDh3Yy93mSd0IwPm3FBrv8hCJgLuv99y6yBSN7pOmOpFjDaCw=="
);
// Other fields preserved
expect(patched).toContain("client_nickname=Bot");
expect(patched).toContain("client_key_offset=42");
expect(patched).toContain("hwid=abc123");
});
});
-52
View File
@@ -1,52 +0,0 @@
/**
* TS6 version used for compatibility. The version_sign is a P-256 ECDSA
* signature verified by TeamSpeak servers; only officially signed pairs work.
*/
const TS6_VERSION = {
version: "3.6.2 [Build: 1695203293]",
platform: "Windows",
sign: "4OH3unxjGNBYS5EN4RFNrEo3UJz2Jn5KW1JqMDh3Yy93mSd0IwPm3FBrv8hCJgLuv99y6yBSN7pOmOpFjDaCw==",
};
/**
* Patch a raw `clientinit` command string to use the 3.6.2 version.
*
* The @honeybbq/teamspeak-client library hardcodes version 3.5.3 in the
* handshake and sends it directly via handler.sendPacket(), bypassing the
* commandMiddleware chain. TS6 servers silently reject 3.5.3 (they never
* respond with `initserver`), causing an idle timeout.
*
* This function is called from a monkey-patched handler.sendPacket() so it
* intercepts the actual handshake packets.
*/
export function patchClientInitVersion(cmd: string): string {
cmd = replaceField(cmd, "client_version", TS6_VERSION.version);
cmd = replaceField(cmd, "client_platform", TS6_VERSION.platform);
cmd = replaceField(cmd, "client_version_sign", TS6_VERSION.sign);
return cmd;
}
/**
* Replace an escaped field value in a TS3 command string.
* Fields are in the format: key=escaped_value (space-separated)
*/
function replaceField(cmd: string, key: string, value: string): string {
// Escape the value using TS3 escaping rules
const escaped = escapeTS3(value);
const regex = new RegExp(`${key}=\\S*`);
if (regex.test(cmd)) {
return cmd.replace(regex, `${key}=${escaped}`);
}
return cmd;
}
function escapeTS3(value: string): string {
return value
.replace(/\\/g, "\\\\")
.replace(/\//g, "\\/")
.replace(/ /g, "\\s")
.replace(/\|/g, "\\p")
.replace(/\n/g, "\\n")
.replace(/\r/g, "\\r")
.replace(/\t/g, "\\t");
}