mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-01 20:42:50 +08:00
feat: add idle timeout setting to auto-disconnect bot when channel is empty
This commit is contained in:
1 parent
d7107677c0
commit
7785cc972b
6 files changed
+120
-1
No files matched your search
Regular → Executable
+49
@@ -60,6 +60,8 @@ export class BotInstance extends EventEmitter {
|
|||||||
private disconnectEmitted = false;
|
private disconnectEmitted = false;
|
||||||
private voteSkipUsers = new Set<string>();
|
private voteSkipUsers = new Set<string>();
|
||||||
private isAdvancing = false;
|
private isAdvancing = false;
|
||||||
|
private idleTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
private channelUserCount = 0;
|
||||||
|
|
||||||
constructor(options: BotInstanceOptions) {
|
constructor(options: BotInstanceOptions) {
|
||||||
super();
|
super();
|
||||||
@@ -121,6 +123,10 @@ export class BotInstance extends EventEmitter {
|
|||||||
this.disconnectEmitted = true;
|
this.disconnectEmitted = true;
|
||||||
this.emit("disconnected");
|
this.emit("disconnected");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.tsClient.on("connected", () => {
|
||||||
|
this._startIdlePoller();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(): Promise<void> {
|
async connect(): Promise<void> {
|
||||||
@@ -138,6 +144,7 @@ export class BotInstance extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
disconnect(): void {
|
disconnect(): void {
|
||||||
|
this._cancelIdleTimer();
|
||||||
this.player.stop();
|
this.player.stop();
|
||||||
this.connected = false;
|
this.connected = false;
|
||||||
if (!this.disconnectEmitted) {
|
if (!this.disconnectEmitted) {
|
||||||
@@ -147,6 +154,48 @@ export class BotInstance extends EventEmitter {
|
|||||||
this.tsClient.disconnect();
|
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> {
|
private async handleTextMessage(msg: TS3TextMessage): Promise<void> {
|
||||||
const parsed = parseCommand(
|
const parsed = parseCommand(
|
||||||
msg.message,
|
msg.message,
|
||||||
|
|||||||
Regular → Executable
+2
@@ -13,6 +13,7 @@ export interface BotConfig {
|
|||||||
adminGroups: number[];
|
adminGroups: number[];
|
||||||
autoReturnDelay: number;
|
autoReturnDelay: number;
|
||||||
autoPauseOnEmpty: boolean;
|
autoPauseOnEmpty: boolean;
|
||||||
|
idleTimeoutMinutes: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaultConfig(): BotConfig {
|
export function getDefaultConfig(): BotConfig {
|
||||||
@@ -28,6 +29,7 @@ export function getDefaultConfig(): BotConfig {
|
|||||||
adminGroups: [],
|
adminGroups: [],
|
||||||
autoReturnDelay: 300,
|
autoReturnDelay: 300,
|
||||||
autoPauseOnEmpty: true,
|
autoPauseOnEmpty: true,
|
||||||
|
idleTimeoutMinutes: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Regular → Executable
+1
@@ -71,6 +71,7 @@ async function main() {
|
|||||||
bilibiliProvider,
|
bilibiliProvider,
|
||||||
database: db,
|
database: db,
|
||||||
config,
|
config,
|
||||||
|
configPath: CONFIG_PATH,
|
||||||
logger,
|
logger,
|
||||||
cookieStore,
|
cookieStore,
|
||||||
staticDir: STATIC_DIR,
|
staticDir: STATIC_DIR,
|
||||||
|
|||||||
Regular → Executable
+23
@@ -1,11 +1,13 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import type { BotManager } from "../../bot/manager.js";
|
import type { BotManager } from "../../bot/manager.js";
|
||||||
import type { BotConfig } from "../../data/config.js";
|
import type { BotConfig } from "../../data/config.js";
|
||||||
|
import { saveConfig } from "../../data/config.js";
|
||||||
import type { Logger } from "../../logger.js";
|
import type { Logger } from "../../logger.js";
|
||||||
|
|
||||||
export function createBotRouter(
|
export function createBotRouter(
|
||||||
botManager: BotManager,
|
botManager: BotManager,
|
||||||
config: BotConfig,
|
config: BotConfig,
|
||||||
|
configPath: string,
|
||||||
logger: Logger
|
logger: Logger
|
||||||
): Router {
|
): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@@ -115,6 +117,27 @@ export function createBotRouter(
|
|||||||
res.status(500).json({ error: (err as Error).message });
|
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;
|
return router;
|
||||||
}
|
}
|
||||||
Regular → Executable
+2
-1
@@ -22,6 +22,7 @@ export interface WebServerOptions {
|
|||||||
bilibiliProvider: MusicProvider;
|
bilibiliProvider: MusicProvider;
|
||||||
database: BotDatabase;
|
database: BotDatabase;
|
||||||
config: BotConfig;
|
config: BotConfig;
|
||||||
|
configPath: string;
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
cookieStore?: CookieStore;
|
cookieStore?: CookieStore;
|
||||||
staticDir?: string;
|
staticDir?: string;
|
||||||
@@ -41,7 +42,7 @@ export function createWebServer(options: WebServerOptions): WebServer {
|
|||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
"/api/bot",
|
"/api/bot",
|
||||||
createBotRouter(options.botManager, options.config, logger)
|
createBotRouter(options.botManager, options.config, options.configPath, logger)
|
||||||
);
|
);
|
||||||
app.use(
|
app.use(
|
||||||
"/api/music",
|
"/api/music",
|
||||||
|
|||||||
Regular → Executable
+43
@@ -371,6 +371,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Idle Timeout -->
|
||||||
|
<section class="settings-section">
|
||||||
|
<h2 class="section-title">行为设置</h2>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-label">
|
||||||
|
<Icon icon="mdi:timer-off-outline" class="setting-icon" />
|
||||||
|
<div>
|
||||||
|
<div>闲置自动退出</div>
|
||||||
|
<div style="font-size:12px; opacity:0.6; margin-top:2px">频道无人时,机器人自动断开的等待时间(0 = 不退出)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="prefix-input-wrap">
|
||||||
|
<input
|
||||||
|
v-model.number="idleTimeout"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
class="input input-sm"
|
||||||
|
style="max-width:80px"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
<span style="font-size:13px; opacity:0.7">分钟</span>
|
||||||
|
<button class="btn-primary" @click="saveIdleTimeout">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -661,10 +687,27 @@ async function savePrefix() {
|
|||||||
// Prefix is saved client-side for now
|
// 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(() => {
|
onMounted(() => {
|
||||||
store.fetchBots(); // Refresh bot status on page visit
|
store.fetchBots(); // Refresh bot status on page visit
|
||||||
checkAuthStatus();
|
checkAuthStatus();
|
||||||
loadQuality();
|
loadQuality();
|
||||||
|
loadIdleTimeout();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|||||||
Reference in new issue
Block a user