mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-01 20:42:50 +08:00
Fix Docker restart crash: respect autoStart flag and persist identity
Root cause: on Docker restart, loadSavedBots() unconditionally connected ALL saved bots regardless of autoStart flag, causing a rapid connect/disconnect loop. Additionally, bot identities were regenerated on every restart, causing TS server conflicts with stale sessions. Changes: - loadSavedBots() now only auto-connects bots with autoStart=true - startBot/stopBot persist autoStart state so restart behavior matches user intent - Bot TS3 identity is persisted to database and reused across restarts - Database schema migrated to include identity column - TS3Client.connect() cleans up existing connection before reconnecting - Stagger bot connections by 1s to avoid overwhelming the TS server https://claude.ai/code/session_01L2kEV2M1QFWMCyPtLU5LgC
This commit is contained in:
4 files changed
+74
-7
No files matched your search
@@ -553,4 +553,8 @@ export class BotInstance extends EventEmitter {
|
|||||||
isConnected(): boolean {
|
isConnected(): boolean {
|
||||||
return this.connected;
|
return this.connected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getIdentityExport(): string | undefined {
|
||||||
|
return this.tsClient.getIdentityExport();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+34
-1
@@ -131,12 +131,25 @@ export class BotManager {
|
|||||||
const bot = this.bots.get(id);
|
const bot = this.bots.get(id);
|
||||||
if (!bot) throw new Error(`Bot ${id} not found`);
|
if (!bot) throw new Error(`Bot ${id} not found`);
|
||||||
await bot.connect();
|
await bot.connect();
|
||||||
|
|
||||||
|
// Mark as autoStart so it reconnects on Docker restart, and persist identity
|
||||||
|
const saved = this.database.getBotInstances().find((i) => i.id === id);
|
||||||
|
if (saved) {
|
||||||
|
this.database.saveBotInstance({ ...saved, autoStart: true });
|
||||||
|
this.persistBotIdentity(saved, bot);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stopBot(id: string): void {
|
stopBot(id: string): void {
|
||||||
const bot = this.bots.get(id);
|
const bot = this.bots.get(id);
|
||||||
if (!bot) throw new Error(`Bot ${id} not found`);
|
if (!bot) throw new Error(`Bot ${id} not found`);
|
||||||
bot.disconnect();
|
bot.disconnect();
|
||||||
|
|
||||||
|
// Mark as not autoStart so it stays stopped on Docker restart
|
||||||
|
const saved = this.database.getBotInstances().find((i) => i.id === id);
|
||||||
|
if (saved) {
|
||||||
|
this.database.saveBotInstance({ ...saved, autoStart: false });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadSavedBots(): Promise<void> {
|
async loadSavedBots(): Promise<void> {
|
||||||
@@ -150,6 +163,7 @@ export class BotManager {
|
|||||||
port: saved.serverPort,
|
port: saved.serverPort,
|
||||||
queryPort: 10011,
|
queryPort: 10011,
|
||||||
nickname: saved.nickname,
|
nickname: saved.nickname,
|
||||||
|
identity: saved.identity || undefined,
|
||||||
defaultChannel: saved.defaultChannel || undefined,
|
defaultChannel: saved.defaultChannel || undefined,
|
||||||
channelPassword: saved.channelPassword || undefined,
|
channelPassword: saved.channelPassword || undefined,
|
||||||
},
|
},
|
||||||
@@ -163,8 +177,11 @@ export class BotManager {
|
|||||||
|
|
||||||
this.bots.set(saved.id, bot);
|
this.bots.set(saved.id, bot);
|
||||||
|
|
||||||
// Auto-connect in background (non-blocking, won't affect other bots)
|
// Only auto-connect bots that have autoStart enabled
|
||||||
|
if (saved.autoStart) {
|
||||||
bot.connect().then(() => {
|
bot.connect().then(() => {
|
||||||
|
// Persist identity after successful connection for future restarts
|
||||||
|
this.persistBotIdentity(saved, bot);
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
{ botId: saved.id, name: saved.name },
|
{ botId: saved.id, name: saved.name },
|
||||||
"Auto-connected saved bot"
|
"Auto-connected saved bot"
|
||||||
@@ -175,6 +192,15 @@ export class BotManager {
|
|||||||
"Failed to auto-connect bot (start manually from Settings)"
|
"Failed to auto-connect bot (start manually from Settings)"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Stagger connections to avoid overwhelming the TS server
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
} else {
|
||||||
|
this.logger.info(
|
||||||
|
{ botId: saved.id, name: saved.name },
|
||||||
|
"Loaded bot (autoStart disabled, not connecting)"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
@@ -183,6 +209,13 @@ export class BotManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private persistBotIdentity(saved: import("../data/database.js").BotInstance, bot: BotInstance): void {
|
||||||
|
const identity = bot.getIdentityExport();
|
||||||
|
if (identity && identity !== saved.identity) {
|
||||||
|
this.database.saveBotInstance({ ...saved, identity });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
shutdown(): void {
|
shutdown(): void {
|
||||||
for (const bot of this.bots.values()) {
|
for (const bot of this.bots.values()) {
|
||||||
bot.disconnect();
|
bot.disconnect();
|
||||||
|
|||||||
+24
-6
@@ -24,6 +24,7 @@ export interface BotInstance {
|
|||||||
defaultChannel: string;
|
defaultChannel: string;
|
||||||
channelPassword: string;
|
channelPassword: string;
|
||||||
autoStart: boolean;
|
autoStart: boolean;
|
||||||
|
identity?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BotDatabase {
|
export interface BotDatabase {
|
||||||
@@ -36,6 +37,15 @@ export interface BotDatabase {
|
|||||||
close(): void;
|
close(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function migrateSchema(db: Database.Database): void {
|
||||||
|
// Add identity column if it doesn't exist (migration for existing databases)
|
||||||
|
const columns = db.prepare("PRAGMA table_info(bot_instances)").all() as Array<{ name: string }>;
|
||||||
|
const hasIdentity = columns.some((c) => c.name === "identity");
|
||||||
|
if (!hasIdentity) {
|
||||||
|
db.exec("ALTER TABLE bot_instances ADD COLUMN identity TEXT");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function initTables(db: Database.Database): void {
|
function initTables(db: Database.Database): void {
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS play_history (
|
CREATE TABLE IF NOT EXISTS play_history (
|
||||||
@@ -58,7 +68,8 @@ function initTables(db: Database.Database): void {
|
|||||||
nickname TEXT NOT NULL,
|
nickname TEXT NOT NULL,
|
||||||
defaultChannel TEXT NOT NULL,
|
defaultChannel TEXT NOT NULL,
|
||||||
channelPassword TEXT NOT NULL,
|
channelPassword TEXT NOT NULL,
|
||||||
autoStart INTEGER NOT NULL DEFAULT 0
|
autoStart INTEGER NOT NULL DEFAULT 0,
|
||||||
|
identity TEXT
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
@@ -67,6 +78,7 @@ export function createDatabase(dbPath: string): BotDatabase {
|
|||||||
const db = new Database(dbPath);
|
const db = new Database(dbPath);
|
||||||
db.pragma("journal_mode = WAL");
|
db.pragma("journal_mode = WAL");
|
||||||
initTables(db);
|
initTables(db);
|
||||||
|
migrateSchema(db);
|
||||||
|
|
||||||
const insertHistory = db.prepare(`
|
const insertHistory = db.prepare(`
|
||||||
INSERT INTO play_history (botId, songId, songName, artist, album, platform, coverUrl)
|
INSERT INTO play_history (botId, songId, songName, artist, album, platform, coverUrl)
|
||||||
@@ -78,8 +90,8 @@ export function createDatabase(dbPath: string): BotDatabase {
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
const upsertInstance = db.prepare(`
|
const upsertInstance = db.prepare(`
|
||||||
INSERT INTO bot_instances (id, name, serverAddress, serverPort, nickname, defaultChannel, channelPassword, autoStart)
|
INSERT INTO bot_instances (id, name, serverAddress, serverPort, nickname, defaultChannel, channelPassword, autoStart, identity)
|
||||||
VALUES (@id, @name, @serverAddress, @serverPort, @nickname, @defaultChannel, @channelPassword, @autoStart)
|
VALUES (@id, @name, @serverAddress, @serverPort, @nickname, @defaultChannel, @channelPassword, @autoStart, @identity)
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
name = excluded.name,
|
name = excluded.name,
|
||||||
serverAddress = excluded.serverAddress,
|
serverAddress = excluded.serverAddress,
|
||||||
@@ -87,7 +99,8 @@ export function createDatabase(dbPath: string): BotDatabase {
|
|||||||
nickname = excluded.nickname,
|
nickname = excluded.nickname,
|
||||||
defaultChannel = excluded.defaultChannel,
|
defaultChannel = excluded.defaultChannel,
|
||||||
channelPassword = excluded.channelPassword,
|
channelPassword = excluded.channelPassword,
|
||||||
autoStart = excluded.autoStart
|
autoStart = excluded.autoStart,
|
||||||
|
identity = excluded.identity
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const selectInstances = db.prepare(`SELECT * FROM bot_instances`);
|
const selectInstances = db.prepare(`SELECT * FROM bot_instances`);
|
||||||
@@ -109,14 +122,19 @@ export function createDatabase(dbPath: string): BotDatabase {
|
|||||||
upsertInstance.run({
|
upsertInstance.run({
|
||||||
...instance,
|
...instance,
|
||||||
autoStart: instance.autoStart ? 1 : 0,
|
autoStart: instance.autoStart ? 1 : 0,
|
||||||
|
identity: instance.identity ?? null,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getBotInstances() {
|
getBotInstances() {
|
||||||
const rows = selectInstances.all() as Array<
|
const rows = selectInstances.all() as Array<
|
||||||
Omit<BotInstance, "autoStart"> & { autoStart: number }
|
Omit<BotInstance, "autoStart" | "identity"> & { autoStart: number; identity: string | null }
|
||||||
>;
|
>;
|
||||||
return rows.map((r) => ({ ...r, autoStart: r.autoStart === 1 }));
|
return rows.map((r) => ({
|
||||||
|
...r,
|
||||||
|
autoStart: r.autoStart === 1,
|
||||||
|
identity: r.identity ?? undefined,
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteBotInstance(id) {
|
deleteBotInstance(id) {
|
||||||
|
|||||||
@@ -53,6 +53,18 @@ export class TS3Client extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async connect(): Promise<void> {
|
async connect(): Promise<void> {
|
||||||
|
// Clean up any existing connection before creating a new one
|
||||||
|
if (this.client) {
|
||||||
|
this.logger.info("Cleaning up previous connection before reconnecting");
|
||||||
|
try {
|
||||||
|
await this.client.disconnect();
|
||||||
|
} catch {
|
||||||
|
// Ignore errors during cleanup
|
||||||
|
}
|
||||||
|
this.client = null;
|
||||||
|
this.clientId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
const addr = `${this.options.host}:${this.options.port}`;
|
const addr = `${this.options.host}:${this.options.port}`;
|
||||||
this.logger.info({ addr }, "Connecting to TeamSpeak server (full client protocol)");
|
this.logger.info({ addr }, "Connecting to TeamSpeak server (full client protocol)");
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user