fix(ws): re-attach stateChange listeners when bot instance is replaced

startBot creates a fresh BotInstance but the WS layer keyed its listener
map by bot.id, so ensureAllBotsAttached skipped the new object and
stateChange events were never broadcast — the player bar only appeared
after a manual refresh. BotManager now extends EventEmitter and emits
"botInstance" whenever a new bot object is created; websocket.ts stores
the bot reference, detaches on replacement, and subscribes to the event
for immediate wiring.

Also enlarges the bot selector (padding 10×20, font 16, min-height 44,
bigger dot/chevron/state icons, wider name) and adds Playwright repro
scripts for the player bar bug and navbar sizing check.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.6 committed 2026-04-10 17:16:29 +08:00
1 parent d5d6abeb1e
commit 6e828b9c2d
6 files changed
+161 -22

No files matched your search

+6 -1
View File
@@ -1,4 +1,5 @@
import crypto from "node:crypto";
import { EventEmitter } from "node:events";
import {
BotInstance,
type BotInstanceOptions,
@@ -28,7 +29,7 @@ export interface CreateBotParams {
serverPassword?: string;
}
export class BotManager {
export class BotManager extends EventEmitter {
private bots = new Map<string, BotInstance>();
private neteaseProvider: MusicProvider;
private qqProvider: MusicProvider;
@@ -46,6 +47,7 @@ export class BotManager {
config: BotConfig,
logger: Logger
) {
super();
this.neteaseProvider = neteaseProvider;
this.qqProvider = qqProvider;
this.bilibiliProvider = bilibiliProvider;
@@ -82,6 +84,7 @@ export class BotManager {
});
this.bots.set(id, bot);
this.emit("botInstance", bot);
this.database.saveBotInstance({
id,
@@ -179,6 +182,7 @@ export class BotManager {
logger: this.logger,
});
this.bots.set(id, bot);
this.emit("botInstance", bot);
await bot.connect();
// Mark as autoStart so it reconnects on Docker restart, and persist identity
this.database.saveBotInstance({ ...saved, autoStart: true });
@@ -229,6 +233,7 @@ export class BotManager {
});
this.bots.set(saved.id, bot);
this.emit("botInstance", bot);
// Only auto-connect bots that have autoStart enabled
if (saved.autoStart) {
+27 -12
View File
@@ -10,8 +10,9 @@ export function setupWebSocket(
): () => void {
const clients = new Set<WebSocket>();
/** Track which bots already have listeners attached */
/** Track which bot instances have listeners attached (keyed by id, storing ref) */
const attachedBots = new Map<string, {
bot: BotInstance;
stateChange: () => void;
connected: () => void;
disconnected: () => void;
@@ -48,8 +49,22 @@ export function setupWebSocket(
}
};
function detachBotListener(id: string): void {
const existing = attachedBots.get(id);
if (!existing) return;
existing.bot.removeListener("stateChange", existing.stateChange);
existing.bot.removeListener("connected", existing.connected);
existing.bot.removeListener("disconnected", existing.disconnected);
attachedBots.delete(id);
}
function attachBotListener(bot: BotInstance): void {
if (attachedBots.has(bot.id)) return;
const existing = attachedBots.get(bot.id);
if (existing) {
if (existing.bot === bot) return; // already attached to this instance
// Bot instance was replaced (e.g. startBot re-created it) — re-attach
detachBotListener(bot.id);
}
const onStateChange = () => {
broadcast({
@@ -81,6 +96,7 @@ export function setupWebSocket(
bot.on("disconnected", onDisconnected);
attachedBots.set(bot.id, {
bot,
stateChange: onStateChange,
connected: onConnected,
disconnected: onDisconnected,
@@ -94,21 +110,20 @@ export function setupWebSocket(
}
}
// Check for newly added bots periodically
// React immediately when a bot instance is created or replaced
const onBotInstance = (bot: BotInstance) => attachBotListener(bot);
botManager.on("botInstance", onBotInstance);
// Safety net: periodically re-check in case any bot was missed
const intervalId = setInterval(ensureAllBotsAttached, 5000);
ensureAllBotsAttached();
return () => {
clearInterval(intervalId);
// Clean up named listeners
for (const bot of botManager.getAllBots()) {
const listeners = attachedBots.get(bot.id);
if (listeners) {
bot.removeListener("stateChange", listeners.stateChange);
bot.removeListener("connected", listeners.connected);
bot.removeListener("disconnected", listeners.disconnected);
}
botManager.removeListener("botInstance", onBotInstance);
// Clean up all attached listeners (detach from stored bot refs, not live map)
for (const id of Array.from(attachedBots.keys())) {
detachBotListener(id);
}
attachedBots.clear();
};
}