mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9e42d543c | ||
|
|
4406eeacbe | ||
|
|
0408f6ce3c | ||
|
|
47b69a1f95 | ||
|
|
0e992f2f5f | ||
|
|
7785cc972b | ||
|
|
4b412e6277 |
No files matched your search
Regular → Executable
+49
@@ -61,6 +61,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;
|
||||
private profileManager: BotProfileManager;
|
||||
|
||||
constructor(options: BotInstanceOptions) {
|
||||
@@ -131,6 +133,10 @@ export class BotInstance extends EventEmitter {
|
||||
this.disconnectEmitted = true;
|
||||
this.emit("disconnected");
|
||||
});
|
||||
|
||||
this.tsClient.on("connected", () => {
|
||||
this._startIdlePoller();
|
||||
});
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
@@ -149,6 +155,7 @@ export class BotInstance extends EventEmitter {
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
this._cancelIdleTimer();
|
||||
this.player.stop();
|
||||
this.connected = false;
|
||||
if (!this.disconnectEmitted) {
|
||||
@@ -158,6 +165,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
@@ -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
@@ -71,6 +71,7 @@ async function main() {
|
||||
bilibiliProvider,
|
||||
database: db,
|
||||
config,
|
||||
configPath: CONFIG_PATH,
|
||||
logger,
|
||||
cookieStore,
|
||||
staticDir: STATIC_DIR,
|
||||
|
||||
@@ -76,7 +76,14 @@ export function createApiServerManager(
|
||||
);
|
||||
} else {
|
||||
const qqModule = (await import("@sansenjian/qq-music-api")) as any;
|
||||
const koaApp = qqModule.default ?? qqModule;
|
||||
// The module's export structure varies between versions:
|
||||
// 2.2.11+: default → Koa app (has .listen)
|
||||
// 2.2.10: default → wrapper object whose .default is the Koa app
|
||||
// older: module itself may be the Koa app
|
||||
const candidate = qqModule.default ?? qqModule;
|
||||
const koaApp = typeof candidate.listen === "function"
|
||||
? candidate
|
||||
: candidate.default ?? null;
|
||||
if (koaApp && typeof koaApp.listen === "function") {
|
||||
qqMusicServer = await new Promise<Server>((resolve, reject) => {
|
||||
const srv = koaApp.listen(options.qqMusicPort, "127.0.0.1", () =>
|
||||
|
||||
@@ -188,6 +188,9 @@ export class TS3Client extends EventEmitter {
|
||||
};
|
||||
|
||||
this.client = new TS3FullClient(this.identity, addr, this.options.nickname, {
|
||||
// Forward server password to the protocol library so it can be
|
||||
// included in clientinit for password-protected servers
|
||||
serverPassword: this.options.serverPassword,
|
||||
logger: {
|
||||
debug: (msg) => this.logger.debug(msg),
|
||||
info: (msg) => this.logger.info(msg),
|
||||
|
||||
Regular → Executable
+23
@@ -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
@@ -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",
|
||||
|
||||
@@ -77,11 +77,33 @@ function selectBot(id: string) {
|
||||
dropdownOpen.value = false;
|
||||
}
|
||||
|
||||
function copyBotLink(id: string) {
|
||||
async function copyBotLink(id: string) {
|
||||
const url = `${window.location.origin}/bot/${id}`;
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
// navigator.clipboard requires a secure context (HTTPS or localhost).
|
||||
// Fall back to a hidden textarea + execCommand('copy') for plain HTTP
|
||||
// access (e.g. when the bot is hosted on a remote IP).
|
||||
try {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(url);
|
||||
} else {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = url;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.opacity = '0';
|
||||
ta.style.pointerEvents = 'none';
|
||||
document.body.appendChild(ta);
|
||||
ta.focus();
|
||||
ta.select();
|
||||
const ok = document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
if (!ok) throw new Error('execCommand copy returned false');
|
||||
}
|
||||
dropdownOpen.value = false;
|
||||
});
|
||||
} catch (err) {
|
||||
// Last-resort: prompt the user with the URL so they can copy manually
|
||||
console.error('Failed to copy bot link', err);
|
||||
window.prompt('复制以下链接:', url);
|
||||
}
|
||||
}
|
||||
|
||||
async function togglePower(bot: { id: string; connected: boolean; name: string }) {
|
||||
|
||||
Regular → Executable
+43
@@ -371,6 +371,32 @@
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in new issue
Block a user