mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
fix(auto-pause): never treat a failed clientlist as "channel empty"; default OFF
Auto-pause within the first seconds of playback (and re-pause after a manual
play) whenever a listener is actually in the channel.
Root cause (confirmed live against a TS3 server): the full-client
`clientlist -uid -away -voice -groups` command TIMES OUT when other clients are
present in the bot's channel. `getClientsInChannel()` catches the error and
returns `[]`, so the occupancy callers computed `userCount = [].length - 1 = -1`,
which `decideOccupancyAction` reads as `-1 <= 0` → "channel empty" → pause. With
the bot alone, clientlist succeeds (returns just the bot), so the bug only
surfaced when someone was listening — exactly the report.
Fix: a connected bot is always a member of its own channel, so a valid query
returns >= 1 (itself). A length of 0 therefore means the query FAILED, not that
the channel is empty. New pure helper `occupancyFromClientList()` maps a
0-length result to `null` ("occupancy unknown"); `refreshOccupancy()` and the
30s idle poller skip the auto-pause / idle-disconnect decision when the count is
unknown instead of mis-reading it as empty. This also removes a latent
false-positive idle-disconnect on the same failed query.
Also default `autoPauseOnEmpty` to OFF (occupancy detection is unreliable on
some servers); users can opt in from Settings.
Verified live with two clients in one channel: clientlist returns 0 → helper
returns null → no false pause (control: bot alone returns 1 → 0 others, normal).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
75f09694a3
commit
d3fd547ea0
6 files changed
+57
-11
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { decideOccupancyAction } from "./auto-pause.js";
|
||||
import { decideOccupancyAction, occupancyFromClientList } from "./auto-pause.js";
|
||||
|
||||
describe("decideOccupancyAction", () => {
|
||||
it("pauses when empty while playing and enabled", () => {
|
||||
@@ -27,3 +27,21 @@ describe("decideOccupancyAction", () => {
|
||||
expect(decideOccupancyAction("paused", true, false, 1)).toBe("resume");
|
||||
});
|
||||
});
|
||||
|
||||
describe("occupancyFromClientList", () => {
|
||||
it("returns null when the query failed (0 clients — bot itself is always present)", () => {
|
||||
// This is the bug fix: a clientlist timeout makes getClientsInChannel()
|
||||
// return [], which must be treated as "unknown", NOT as an empty channel.
|
||||
expect(occupancyFromClientList(0)).toBeNull();
|
||||
});
|
||||
it("returns 0 other users when only the bot is in the channel", () => {
|
||||
expect(occupancyFromClientList(1)).toBe(0);
|
||||
});
|
||||
it("excludes the bot itself from the count", () => {
|
||||
expect(occupancyFromClientList(2)).toBe(1);
|
||||
expect(occupancyFromClientList(5)).toBe(4);
|
||||
});
|
||||
it("never yields a negative count (guards the -1 that caused false pauses)", () => {
|
||||
expect(occupancyFromClientList(-3)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,25 @@
|
||||
export type PlayerStateName = "idle" | "playing" | "paused";
|
||||
export type OccupancyAction = "pause" | "resume" | "none";
|
||||
|
||||
/**
|
||||
* Convert a channel client-list length into the number of *other* users, or
|
||||
* `null` when occupancy can't be determined.
|
||||
*
|
||||
* A connected bot is always a member of its own channel, so a valid query
|
||||
* returns at least 1 (the bot itself). A length of 0 therefore does NOT mean
|
||||
* "empty channel" — it means the underlying `clientlist` query failed (e.g. the
|
||||
* full-client `clientlist` command times out when other clients are present,
|
||||
* and `getClientsInChannel()` returns `[]` on error). Treating that failure as
|
||||
* "empty" is what caused playback to auto-pause within seconds whenever a
|
||||
* listener was actually in the channel. When the result is indeterminate we
|
||||
* return `null` so callers skip the auto-pause/idle decision entirely rather
|
||||
* than mis-reading an unknown state as empty.
|
||||
*/
|
||||
export function occupancyFromClientList(clientCount: number): number | null {
|
||||
if (clientCount <= 0) return null; // query failed → occupancy unknown
|
||||
return clientCount - 1; // exclude the bot itself
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide what auto-pause should do given channel occupancy.
|
||||
* - empty (userCount <= 0): pause iff enabled and currently playing.
|
||||
|
||||
+9
-4
@@ -18,7 +18,7 @@ import type { BotDatabase, ProfileConfig } from "../data/database.js";
|
||||
import type { BotConfig } from "../data/config.js";
|
||||
import { BotProfileManager } from "./profile.js";
|
||||
import type { AvatarStore } from "../data/avatars.js";
|
||||
import { decideOccupancyAction } from "./auto-pause.js";
|
||||
import { decideOccupancyAction, occupancyFromClientList } from "./auto-pause.js";
|
||||
|
||||
export interface BotInstanceOptions {
|
||||
id: string;
|
||||
@@ -175,7 +175,11 @@ export class BotInstance extends EventEmitter {
|
||||
if (!this.connected) return;
|
||||
try {
|
||||
const clients = await this.tsClient.getClientsInChannel();
|
||||
this.handleOccupancy(clients.length - 1);
|
||||
// A 0-length result means the clientlist query failed (the bot is always
|
||||
// in its own channel) — occupancy is unknown, so don't act. Acting on it
|
||||
// would mis-read it as "empty" and falsely auto-pause / idle-disconnect.
|
||||
const userCount = occupancyFromClientList(clients.length);
|
||||
if (userCount !== null) this.handleOccupancy(userCount);
|
||||
} catch {
|
||||
// ignore — the 30s poll is the fallback
|
||||
}
|
||||
@@ -229,8 +233,9 @@ export class BotInstance extends EventEmitter {
|
||||
if (!this.connected) return;
|
||||
try {
|
||||
const clients = await this.tsClient.getClientsInChannel();
|
||||
const userCount = clients.length - 1; // 排除 bot 自身
|
||||
this.handleOccupancy(userCount);
|
||||
// null = clientlist query failed (occupancy unknown) → don't act.
|
||||
const userCount = occupancyFromClientList(clients.length);
|
||||
if (userCount !== null) this.handleOccupancy(userCount);
|
||||
} catch { /* ignore */ }
|
||||
setTimeout(poll, 30_000);
|
||||
};
|
||||
|
||||
@@ -49,7 +49,8 @@ describe("config", () => {
|
||||
// defaults should fill in the rest
|
||||
expect(loaded.theme).toBe("dark");
|
||||
expect(loaded.commandPrefix).toBe("!");
|
||||
expect(loaded.autoPauseOnEmpty).toBe(true);
|
||||
// auto-pause defaults OFF (occupancy detection is unreliable on some servers)
|
||||
expect(loaded.autoPauseOnEmpty).toBe(false);
|
||||
});
|
||||
|
||||
// --- #86: config.json must live under (and be created in) the persisted data dir ---
|
||||
|
||||
+4
-1
@@ -36,7 +36,10 @@ export function getDefaultConfig(): BotConfig {
|
||||
adminPassword: "",
|
||||
adminGroups: [],
|
||||
autoReturnDelay: 300,
|
||||
autoPauseOnEmpty: true,
|
||||
// Default OFF: occupancy detection relies on the full-client `clientlist`
|
||||
// command, which is unreliable on some servers (it can time out when other
|
||||
// clients are present). Users can opt in from the web UI.
|
||||
autoPauseOnEmpty: false,
|
||||
idleTimeoutMinutes: 0,
|
||||
publicUrl: "",
|
||||
trustProxy: false,
|
||||
|
||||
Reference in new issue
Block a user