feat: add idle timeout setting to auto-disconnect bot when channel is empty

This commit is contained in:
NeoPecos committed 2026-04-13 17:27:51 +08:00
1 parent d7107677c0
commit 7785cc972b
6 files changed
+120 -1

No files matched your search

Regular → Executable
+49
View File
@@ -60,6 +60,8 @@ export class BotInstance extends EventEmitter {
private disconnectEmitted = false;
private voteSkipUsers = new Set<string>();
private isAdvancing = false;
private idleTimer: ReturnType<typeof setTimeout> | null = null;
private channelUserCount = 0;
constructor(options: BotInstanceOptions) {
super();
@@ -121,6 +123,10 @@ export class BotInstance extends EventEmitter {
this.disconnectEmitted = true;
this.emit("disconnected");
});
this.tsClient.on("connected", () => {
this._startIdlePoller();
});
}
async connect(): Promise<void> {
@@ -138,6 +144,7 @@ export class BotInstance extends EventEmitter {
}
disconnect(): void {
this._cancelIdleTimer();
this.player.stop();
this.connected = false;
if (!this.disconnectEmitted) {
@@ -147,6 +154,48 @@ export class BotInstance extends EventEmitter {
this.tsClient.disconnect();
}
/** 外部更新 idleTimeoutMinutes(由 API 保存时调用) */
updateIdleTimeout(minutes: number): void {
this.config.idleTimeoutMinutes = minutes;
if (minutes === 0) this._cancelIdleTimer();
}
private _startIdlePoller(): void {
// 每 30 秒检查一次频道人数
const poll = async () => {
if (!this.connected) return;
try {
const clients = await this.tsClient.getClientsInChannel();
const userCount = clients.length - 1; // 排除 bot 自身
if (userCount <= 0) {
this._scheduleIdleCheck();
} else {
this._cancelIdleTimer();
}
} catch { /* ignore */ }
setTimeout(poll, 30_000);
};
setTimeout(poll, 30_000);
}
private _scheduleIdleCheck(): void {
if (this.idleTimer !== null) return; // 已经在倒计时,不重复创建
const minutes = this.config.idleTimeoutMinutes ?? 0;
if (!this.connected || minutes <= 0) return;
this.idleTimer = setTimeout(() => {
if (!this.connected) return;
this.logger.info({ idleMinutes: minutes }, "Channel empty, disconnecting due to idle timeout");
this.disconnect();
}, minutes * 60 * 1000);
}
private _cancelIdleTimer(): void {
if (this.idleTimer) {
clearTimeout(this.idleTimer);
this.idleTimer = null;
}
}
private async handleTextMessage(msg: TS3TextMessage): Promise<void> {
const parsed = parseCommand(
msg.message,
Regular → Executable
+2
View File
@@ -13,6 +13,7 @@ export interface BotConfig {
adminGroups: number[];
autoReturnDelay: number;
autoPauseOnEmpty: boolean;
idleTimeoutMinutes: number;
}
export function getDefaultConfig(): BotConfig {
@@ -28,6 +29,7 @@ export function getDefaultConfig(): BotConfig {
adminGroups: [],
autoReturnDelay: 300,
autoPauseOnEmpty: true,
idleTimeoutMinutes: 0,
};
}
Regular → Executable
+1
View File
@@ -71,6 +71,7 @@ async function main() {
bilibiliProvider,
database: db,
config,
configPath: CONFIG_PATH,
logger,
cookieStore,
staticDir: STATIC_DIR,
Regular → Executable
+23
View File
@@ -1,11 +1,13 @@
import { Router } from "express";
import type { BotManager } from "../../bot/manager.js";
import type { BotConfig } from "../../data/config.js";
import { saveConfig } from "../../data/config.js";
import type { Logger } from "../../logger.js";
export function createBotRouter(
botManager: BotManager,
config: BotConfig,
configPath: string,
logger: Logger
): Router {
const router = Router();
@@ -115,6 +117,27 @@ export function createBotRouter(
res.status(500).json({ error: (err as Error).message });
}
});
// GET /api/bot/settings — 读取全局 bot 行为设置
router.get("/settings", (_req, res) => {
res.json({ idleTimeoutMinutes: config.idleTimeoutMinutes ?? 0 });
});
// POST /api/bot/settings — 保存全局 bot 行为设置
router.post("/settings", (req, res) => {
const { idleTimeoutMinutes } = req.body;
if (typeof idleTimeoutMinutes !== "number" || idleTimeoutMinutes < 0) {
res.status(400).json({ error: "idleTimeoutMinutes must be a non-negative number" });
return;
}
config.idleTimeoutMinutes = idleTimeoutMinutes;
saveConfig(configPath, config);
// 通知所有 bot 实例更新定时器
for (const bot of botManager.getAllBots()) {
bot.updateIdleTimeout(idleTimeoutMinutes);
}
res.json({ ok: true });
});
return router;
}
Regular → Executable
+2 -1
View File
@@ -22,6 +22,7 @@ export interface WebServerOptions {
bilibiliProvider: MusicProvider;
database: BotDatabase;
config: BotConfig;
configPath: string;
logger: Logger;
cookieStore?: CookieStore;
staticDir?: string;
@@ -41,7 +42,7 @@ export function createWebServer(options: WebServerOptions): WebServer {
app.use(
"/api/bot",
createBotRouter(options.botManager, options.config, logger)
createBotRouter(options.botManager, options.config, options.configPath, logger)
);
app.use(
"/api/music",