diff --git a/src/bot/instance.ts b/src/bot/instance.ts old mode 100644 new mode 100755 index 3b1f49a..553e7aa --- a/src/bot/instance.ts +++ b/src/bot/instance.ts @@ -61,7 +61,11 @@ export class BotInstance extends EventEmitter { private disconnectEmitted = false; private voteSkipUsers = new Set(); private isAdvancing = false; +<<<<<<< HEAD + private idleTimer: ReturnType | null = null; + private channelUserCount = 0; private profileManager: BotProfileManager; +>>>>>>> origin/main constructor(options: BotInstanceOptions) { super(); @@ -131,6 +135,10 @@ export class BotInstance extends EventEmitter { this.disconnectEmitted = true; this.emit("disconnected"); }); + + this.tsClient.on("connected", () => { + this._startIdlePoller(); + }); } async connect(): Promise { @@ -149,6 +157,7 @@ export class BotInstance extends EventEmitter { } disconnect(): void { + this._cancelIdleTimer(); this.player.stop(); this.connected = false; if (!this.disconnectEmitted) { @@ -158,6 +167,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 { const parsed = parseCommand( msg.message, diff --git a/src/data/config.ts b/src/data/config.ts old mode 100644 new mode 100755 index 2b26897..c1a67a5 --- a/src/data/config.ts +++ b/src/data/config.ts @@ -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, }; } diff --git a/src/index.ts b/src/index.ts old mode 100644 new mode 100755 index 14beb61..2f75fd6 --- a/src/index.ts +++ b/src/index.ts @@ -71,6 +71,7 @@ async function main() { bilibiliProvider, database: db, config, + configPath: CONFIG_PATH, logger, cookieStore, staticDir: STATIC_DIR, diff --git a/src/web/api/bot.ts b/src/web/api/bot.ts old mode 100644 new mode 100755 index e60bece..50a135f --- a/src/web/api/bot.ts +++ b/src/web/api/bot.ts @@ -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; } diff --git a/src/web/server.ts b/src/web/server.ts old mode 100644 new mode 100755 index 7a5621d..cb6d85d --- a/src/web/server.ts +++ b/src/web/server.ts @@ -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", diff --git a/web/src/views/Settings.vue b/web/src/views/Settings.vue old mode 100644 new mode 100755 index 7bb2128..ca90e21 --- a/web/src/views/Settings.vue +++ b/web/src/views/Settings.vue @@ -371,6 +371,32 @@ + + +
+

行为设置

+
+
+ +
+
闲置自动退出
+
频道无人时,机器人自动断开的等待时间(0 = 不退出)
+
+
+
+ + 分钟 + +
+
+
@@ -661,10 +687,27 @@ async function savePrefix() { // Prefix is saved client-side for now } +// Idle timeout +const idleTimeout = ref(0); + +async function loadIdleTimeout() { + try { + const res = await axios.get('/api/bot/settings'); + idleTimeout.value = res.data.idleTimeoutMinutes ?? 0; + } catch { /* ignore */ } +} + +async function saveIdleTimeout() { + try { + await axios.post('/api/bot/settings', { idleTimeoutMinutes: idleTimeout.value }); + } catch { /* ignore */ } +} + onMounted(() => { store.fetchBots(); // Refresh bot status on page visit checkAuthStatus(); loadQuality(); + loadIdleTimeout(); }); onUnmounted(() => {