From 42446950753d9ef9b5a612865abd1b9d3220d3f8 Mon Sep 17 00:00:00 2001 From: Dr1mH4X <102709153+Dr1mH4X@users.noreply.github.com> Date: Tue, 23 Jun 2026 02:37:13 +0800 Subject: [PATCH 1/3] feat: Add channelId support to bot configuration and database --- src/bot/manager.ts | 36 ++++++---- src/data/database.test.ts | 3 + src/data/database.ts | 11 ++- src/ts-protocol/client.ts | 139 ++++++++++++++++++++++++++----------- src/web/api/bot.ts | 6 +- web/src/views/Settings.vue | 28 ++++++-- web/src/views/Setup.vue | 14 +++- 7 files changed, 169 insertions(+), 68 deletions(-) diff --git a/src/bot/manager.ts b/src/bot/manager.ts index 5748da1..ebaf706 100644 --- a/src/bot/manager.ts +++ b/src/bot/manager.ts @@ -57,6 +57,7 @@ export interface CreateBotParams { queryPort?: number; nickname: string; defaultChannel?: string; + channelId?: string; channelPassword?: string; autoStart?: boolean; /** Force TS3 or TS6 protocol; omit or "unknown" for auto-detect. */ @@ -113,6 +114,7 @@ export class BotManager extends EventEmitter { queryPort: params.queryPort ?? 10011, nickname: params.nickname, defaultChannel: params.defaultChannel, + channelId: params.channelId, channelPassword: params.channelPassword, serverPassword: params.serverPassword, serverProtocol: params.serverProtocol, @@ -138,6 +140,7 @@ export class BotManager extends EventEmitter { serverPort: params.serverPort, nickname: params.nickname, defaultChannel: params.defaultChannel ?? "", + channelId: params.channelId ?? "", channelPassword: params.channelPassword ?? "", autoStart: params.autoStart ?? false, serverProtocol: params.serverProtocol ?? "", @@ -173,6 +176,7 @@ export class BotManager extends EventEmitter { serverPort: params.serverPort ?? existing.serverPort, nickname: params.nickname ?? existing.nickname, defaultChannel: params.defaultChannel ?? existing.defaultChannel, + channelId: params.channelId ?? existing.channelId, channelPassword: params.channelPassword ?? existing.channelPassword, serverProtocol: params.serverProtocol ?? existing.serverProtocol, ts6ApiKey: params.ts6ApiKey ?? existing.ts6ApiKey, @@ -221,21 +225,22 @@ export class BotManager extends EventEmitter { const bot = new BotInstance({ id: saved.id, name: saved.name, - tsOptions: { - host: saved.serverAddress, - port: saved.serverPort, - queryPort: proto === "ts6" ? 10080 : 10011, - nickname: saved.nickname, - // Reuse the stored identity so server groups assigned to this bot - // survive restarts — without this the TS server sees a new UID - // each connect and strips all previously granted groups. - identity: saved.identity || undefined, - defaultChannel: saved.defaultChannel || undefined, - channelPassword: saved.channelPassword || undefined, - serverPassword: saved.serverPassword || undefined, - serverProtocol: proto === "ts3" || proto === "ts6" ? proto : undefined, - ts6ApiKey: saved.ts6ApiKey || undefined, - }, + tsOptions: { + host: saved.serverAddress, + port: saved.serverPort, + queryPort: proto === "ts6" ? 10080 : 10011, + nickname: saved.nickname, + // Reuse the stored identity so server groups assigned to this bot + // survive restarts — without this the TS server sees a new UID + // each connect and strips all previously granted groups. + identity: saved.identity || undefined, + defaultChannel: saved.defaultChannel || undefined, + channelId: saved.channelId || undefined, + channelPassword: saved.channelPassword || undefined, + serverPassword: saved.serverPassword || undefined, + serverProtocol: proto === "ts3" || proto === "ts6" ? proto : undefined, + ts6ApiKey: saved.ts6ApiKey || undefined, + }, neteaseProvider: this.neteaseProvider, qqProvider: this.qqProvider, bilibiliProvider: this.bilibiliProvider, @@ -282,6 +287,7 @@ export class BotManager extends EventEmitter { nickname: saved.nickname, identity: saved.identity || undefined, defaultChannel: saved.defaultChannel || undefined, + channelId: saved.channelId || undefined, channelPassword: saved.channelPassword || undefined, serverPassword: saved.serverPassword || undefined, serverProtocol: proto === "ts3" || proto === "ts6" ? proto : undefined, diff --git a/src/data/database.test.ts b/src/data/database.test.ts index 7fc96f0..52569ff 100644 --- a/src/data/database.test.ts +++ b/src/data/database.test.ts @@ -82,6 +82,7 @@ describe("database", () => { serverPort: 9987, nickname: "MusicBot", defaultChannel: "Music", + channelId: "", channelPassword: "", autoStart: true, serverProtocol: "", @@ -111,6 +112,7 @@ describe("database", () => { serverPort: 9987, nickname: "MusicBot", defaultChannel: "Music", + channelId: "", channelPassword: "", autoStart: false, serverProtocol: "", @@ -131,6 +133,7 @@ describe("database", () => { serverPort: 9987, nickname: "n", defaultChannel: "", + channelId: "", channelPassword: "", autoStart: false, serverProtocol: "", diff --git a/src/data/database.ts b/src/data/database.ts index d2d9a43..ef6b171 100644 --- a/src/data/database.ts +++ b/src/data/database.ts @@ -23,6 +23,7 @@ export interface BotInstance { serverPort: number; nickname: string; defaultChannel: string; + channelId: string; channelPassword: string; autoStart: boolean; /** "ts3" | "ts6" | "" (empty = auto-detect) */ @@ -96,6 +97,9 @@ function migrateSchema(db: Database.Database): void { if (!names.includes("serverPassword")) { db.exec("ALTER TABLE bot_instances ADD COLUMN serverPassword TEXT NOT NULL DEFAULT ''"); } + if (!names.includes("channelId")) { + db.exec("ALTER TABLE bot_instances ADD COLUMN channelId TEXT NOT NULL DEFAULT ''"); + } // Profile feature flags const profileCols = [ "profile_avatar_enabled", @@ -142,6 +146,7 @@ function initTables(db: Database.Database): void { serverPort INTEGER NOT NULL, nickname TEXT NOT NULL, defaultChannel TEXT NOT NULL, + channelId TEXT NOT NULL DEFAULT '', channelPassword TEXT NOT NULL, autoStart INTEGER NOT NULL DEFAULT 0, serverProtocol TEXT NOT NULL DEFAULT '', @@ -254,14 +259,15 @@ export function createDatabase(dbPath: string): BotDatabase { `); const upsertInstance = db.prepare(` - INSERT INTO bot_instances (id, name, serverAddress, serverPort, nickname, defaultChannel, channelPassword, autoStart, serverProtocol, ts6ApiKey, serverPassword, identity) - VALUES (@id, @name, @serverAddress, @serverPort, @nickname, @defaultChannel, @channelPassword, @autoStart, @serverProtocol, @ts6ApiKey, @serverPassword, @identity) + INSERT INTO bot_instances (id, name, serverAddress, serverPort, nickname, defaultChannel, channelId, channelPassword, autoStart, serverProtocol, ts6ApiKey, serverPassword, identity) + VALUES (@id, @name, @serverAddress, @serverPort, @nickname, @defaultChannel, @channelId, @channelPassword, @autoStart, @serverProtocol, @ts6ApiKey, @serverPassword, @identity) ON CONFLICT(id) DO UPDATE SET name = excluded.name, serverAddress = excluded.serverAddress, serverPort = excluded.serverPort, nickname = excluded.nickname, defaultChannel = excluded.defaultChannel, + channelId = excluded.channelId, channelPassword = excluded.channelPassword, autoStart = excluded.autoStart, serverProtocol = excluded.serverProtocol, @@ -342,6 +348,7 @@ export function createDatabase(dbPath: string): BotDatabase { serverProtocol: r.serverProtocol ?? "", ts6ApiKey: r.ts6ApiKey ?? "", serverPassword: r.serverPassword ?? "", + channelId: r.channelId ?? "", identity: r.identity ?? undefined, })); }, diff --git a/src/ts-protocol/client.ts b/src/ts-protocol/client.ts index 383c6fc..bf27821 100644 --- a/src/ts-protocol/client.ts +++ b/src/ts-protocol/client.ts @@ -46,6 +46,7 @@ export interface TS3ClientOptions { nickname: string; identity?: string; // Exported identity string, or undefined to generate new defaultChannel?: string; + channelId?: string; // Numeric channel ID (takes precedence over defaultChannel) channelPassword?: string; serverPassword?: string; /** Force a specific protocol instead of auto-detecting. */ @@ -72,7 +73,10 @@ export class TS3Client extends EventEmitter { private httpQuery: TS6HttpQuery | null = null; private udpErrorTimer: ReturnType | null = null; - constructor(private options: TS3ClientOptions, logger: Logger) { + constructor( + private options: TS3ClientOptions, + logger: Logger, + ) { super(); this.logger = logger; @@ -109,7 +113,10 @@ export class TS3Client extends EventEmitter { const addr = `${this.options.host}:${this.options.port}`; // Detect or use forced protocol - if (this.options.serverProtocol && this.options.serverProtocol !== "unknown") { + if ( + this.options.serverProtocol && + this.options.serverProtocol !== "unknown" + ) { this.detectedProtocol = this.options.serverProtocol; this.logger.info( { addr, protocol: this.detectedProtocol }, @@ -132,7 +139,11 @@ export class TS3Client extends EventEmitter { ); } else { this.logger.info( - { addr, protocol: this.detectedProtocol, queryPort: detection.queryPort }, + { + addr, + protocol: this.detectedProtocol, + queryPort: detection.queryPort, + }, `Server protocol detected: ${this.detectedProtocol.toUpperCase()}`, ); } @@ -140,7 +151,8 @@ export class TS3Client extends EventEmitter { // Set up TS6 HTTP Query if applicable if (this.detectedProtocol === "ts6") { - const queryPort = this.options.queryPort !== 10011 ? this.options.queryPort : 10080; + const queryPort = + this.options.queryPort !== 10011 ? this.options.queryPort : 10080; this.httpQuery = new TS6HttpQuery({ host: this.options.host, port: queryPort, @@ -151,7 +163,9 @@ export class TS3Client extends EventEmitter { // Guard against calling connect() while already connected. // Save detectedProtocol first because disconnect() resets it. if (this.client) { - this.logger.warn("connect() called while already connected, disconnecting first"); + this.logger.warn( + "connect() called while already connected, disconnecting first", + ); const savedProtocol = this.detectedProtocol; const savedHttpQuery = this.httpQuery; this.disconnect(); @@ -178,7 +192,9 @@ export class TS3Client extends EventEmitter { if (this.udpErrorTimer) clearTimeout(this.udpErrorTimer); this.udpErrorTimer = setTimeout(() => { if (udpErrorCount > 1) { - this.logger.warn(`udp send error (repeated ${udpErrorCount} times, connection may be lost)`); + this.logger.warn( + `udp send error (repeated ${udpErrorCount} times, connection may be lost)`, + ); } udpErrorCount = 0; this.udpErrorTimer = null; @@ -189,17 +205,22 @@ export class TS3Client extends EventEmitter { this.logger.warn(msg); }; - 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), - warn: throttledWarn, - error: (msg) => this.logger.error(msg), + 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), + warn: throttledWarn, + error: (msg) => this.logger.error(msg), + }, }, - }); + ); this.client.on("textMessage", (msg: TextMessage) => { const tsMsg: TS3TextMessage = { @@ -221,7 +242,7 @@ export class TS3Client extends EventEmitter { this.client.on("clientEnter", (info: ClientInfo) => { this.logger.debug( { nickname: info.nickname, id: info.id }, - "Client entered" + "Client entered", ); this.emit("clientEnter", info); }); @@ -234,7 +255,7 @@ export class TS3Client extends EventEmitter { this.client.on("clientMoved", (ev: ClientMovedEvent) => { this.logger.debug( { id: ev.id, targetChannelID: ev.targetChannelID.toString() }, - "Client moved" + "Client moved", ); this.emit("clientMoved", ev); }); @@ -254,11 +275,16 @@ export class TS3Client extends EventEmitter { `Logged in (visible client, ${this.detectedProtocol.toUpperCase()} server)`, ); - // Join default channel if specified - if (this.options.defaultChannel) { + // Join channel by numeric ID (takes precedence) or by name + if (this.options.channelId) { + await this.joinChannel( + this.options.channelId, + this.options.channelPassword, + ); + } else if (this.options.defaultChannel) { await this.joinChannel( this.options.defaultChannel, - this.options.channelPassword + this.options.channelPassword, ); } @@ -268,24 +294,32 @@ export class TS3Client extends EventEmitter { async joinChannel(channelName: string, password?: string): Promise { if (!this.client) return; + const isNumeric = /^\d+$/.test(channelName); + if (isNumeric) { + try { + await clientMove( + this.client, + this.clientId, + BigInt(channelName), + password, + ); + } catch (err) { + this.logger.error({ err, channelName }, "Failed to join channel"); + } + return; + } + try { const channels = await listChannels(this.client); const channel = channels.find((ch) => ch.name === channelName); - if (!channel) { this.logger.warn({ channelName }, "Channel not found"); return; } - - await clientMove( - this.client, - this.clientId, - channel.id, - password - ); + await clientMove(this.client, this.clientId, channel.id, password); this.logger.info( { channelName, cid: channel.id.toString() }, - "Joined channel" + "Joined channel", ); } catch (err) { this.logger.error({ err, channelName }, "Failed to join channel"); @@ -294,7 +328,7 @@ export class TS3Client extends EventEmitter { async sendTextMessage( message: string, - targetMode: number = 2 + targetMode: number = 2, ): Promise { if (!this.client) return; // targetMode 2 = channel, target 0 = current channel @@ -326,7 +360,9 @@ export class TS3Client extends EventEmitter { await this.client.sendCommandNoWait(cmd); } - async execCommandWithResponse(cmd: string): Promise[]> { + async execCommandWithResponse( + cmd: string, + ): Promise[]> { if (!this.client) throw new Error("Not connected"); return this.client.execCommandWithResponse(cmd); } @@ -339,15 +375,28 @@ export class TS3Client extends EventEmitter { overwrite = true, ): Promise { if (!this.client) throw new Error("Not connected"); - return this.client.fileTransferInitUpload(channelID, path, password, size, overwrite); + return this.client.fileTransferInitUpload( + channelID, + path, + password, + size, + overwrite, + ); } - async uploadFileData(host: string, info: FileUploadInfo, data: Readable): Promise { + async uploadFileData( + host: string, + info: FileUploadInfo, + data: Readable, + ): Promise { if (!this.client) throw new Error("Not connected"); await this.client.uploadFileData(host, info, data); } - async fileTransferDeleteFile(channelID: bigint, paths: string[]): Promise { + async fileTransferDeleteFile( + channelID: bigint, + paths: string[], + ): Promise { if (!this.client) throw new Error("Not connected"); await fileTransferDeleteFile(this.client, channelID, paths); } @@ -371,7 +420,10 @@ export class TS3Client extends EventEmitter { this.client.sendVoice(opusFrame, 5); this.voiceFramesSent++; if (this.voiceFramesSent === 1) { - this.logger.info({ opusBytes: opusFrame.length, clientId: this.clientId }, "First voice packet sent to TeamSpeak"); + this.logger.info( + { opusBytes: opusFrame.length, clientId: this.clientId }, + "First voice packet sent to TeamSpeak", + ); } } catch (err) { if (this.voiceFramesSent === 0) { @@ -392,12 +444,15 @@ export class TS3Client extends EventEmitter { if (this.client && !this.disconnecting) { this.disconnecting = true; const client = this.client; - client.disconnect().catch(() => {}).finally(() => { - if (this.client === client) { - this.client = null; - } - this.disconnecting = false; - }); + client + .disconnect() + .catch(() => {}) + .finally(() => { + if (this.client === client) { + this.client = null; + } + this.disconnecting = false; + }); } this.clientId = 0; this.httpQuery = null; diff --git a/src/web/api/bot.ts b/src/web/api/bot.ts index 85172de..35e9dea 100755 --- a/src/web/api/bot.ts +++ b/src/web/api/bot.ts @@ -159,6 +159,7 @@ export function createBotRouter( serverPort, nickname, defaultChannel, + channelId, channelPassword, serverPassword, autoStart, @@ -175,6 +176,7 @@ export function createBotRouter( serverPort: serverPort ?? 9987, nickname, defaultChannel, + channelId, channelPassword, serverPassword, autoStart: autoStart ?? false, @@ -194,10 +196,10 @@ export function createBotRouter( res.status(404).json({ error: "Bot not found" }); return; } - const { name, serverAddress, serverPort, nickname, defaultChannel, channelPassword, serverPassword } = req.body; + const { name, serverAddress, serverPort, nickname, defaultChannel, channelId, channelPassword, serverPassword } = req.body; // Update in database botManager.updateBot(req.params.id, { - name, serverAddress, serverPort, nickname, defaultChannel, channelPassword, serverPassword, + name, serverAddress, serverPort, nickname, defaultChannel, channelId, channelPassword, serverPassword, }); res.json({ success: true }); } catch (err) { diff --git a/web/src/views/Settings.vue b/web/src/views/Settings.vue index e12585b..cd0fbf1 100755 --- a/web/src/views/Settings.vue +++ b/web/src/views/Settings.vue @@ -98,8 +98,12 @@
- - + + +
+
+ +
@@ -142,8 +146,12 @@
- - + + +
+
+ +
@@ -408,7 +416,7 @@
- +

行为设置

@@ -685,6 +693,7 @@ const newBotServer = ref(''); const newBotPort = ref(9987); const newBotNickname = ref('MusicBot'); const newBotChannel = ref(''); +const newBotChannelId = ref(''); const newBotServerPassword = ref(''); const newBotAvatar = ref(null); @@ -696,6 +705,7 @@ const editForm = reactive({ serverPort: 9987, nickname: '', defaultChannel: '', + channelId: '', channelPassword: '', serverPassword: '', }); @@ -853,6 +863,7 @@ async function createBot() { serverPort: newBotPort.value || 9987, nickname: newBotNickname.value || newBotName.value, defaultChannel: newBotChannel.value || undefined, + channelId: newBotChannelId.value || undefined, serverPassword: newBotServerPassword.value || undefined, autoStart: false, }); @@ -868,6 +879,7 @@ async function createBot() { newBotPort.value = 9987; newBotNickname.value = 'MusicBot'; newBotChannel.value = ''; + newBotChannelId.value = ''; newBotServerPassword.value = ''; newBotAvatar.value = null; await store.fetchBots(); @@ -901,6 +913,7 @@ async function openEditBot(bot: any) { editForm.serverPort = res.data.serverPort ?? 9987; editForm.nickname = res.data.nickname ?? ''; editForm.defaultChannel = res.data.defaultChannel ?? ''; + editForm.channelId = res.data.channelId ?? ''; editForm.channelPassword = res.data.channelPassword ?? ''; editForm.serverPassword = res.data.serverPassword ?? ''; } catch { @@ -909,6 +922,7 @@ async function openEditBot(bot: any) { editForm.serverPort = 9987; editForm.nickname = bot.name; editForm.defaultChannel = ''; + editForm.channelId = ''; editForm.channelPassword = ''; editForm.serverPassword = ''; } @@ -1635,6 +1649,10 @@ onUnmounted(() => { font-size: 13px; outline: none; &:focus { border-color: var(--color-primary); } + &.disabled { + opacity: 0.4; + cursor: not-allowed; + } } .input-sm { max-width: 80px; } diff --git a/web/src/views/Setup.vue b/web/src/views/Setup.vue index b82dec6..6bf2d23 100644 --- a/web/src/views/Setup.vue +++ b/web/src/views/Setup.vue @@ -46,8 +46,12 @@
- - + + +
+
+ +
@@ -87,6 +91,7 @@ const serverAddress = ref(''); const serverPort = ref(9987); const nickname = ref('MusicBot'); const defaultChannel = ref(''); +const channelId = ref(''); async function createBotAndNext() { try { @@ -96,6 +101,7 @@ async function createBotAndNext() { serverPort: serverPort.value, nickname: nickname.value, defaultChannel: defaultChannel.value, + channelId: channelId.value || undefined, autoStart: true, }); currentStep.value = 2; @@ -193,6 +199,10 @@ async function createBotAndNext() { &:focus { border-color: var(--color-primary); } + &.disabled { + opacity: 0.4; + cursor: not-allowed; + } } .btn-primary { From 05f090d83a060300a3b1569e88a5f30ae3946626 Mon Sep 17 00:00:00 2001 From: Dr1mH4X <102709153+Dr1mH4X@users.noreply.github.com> Date: Tue, 23 Jun 2026 02:40:54 +0800 Subject: [PATCH 2/3] chore: format --- src/ts-protocol/client.ts | 119 ++++++++++++-------------------------- 1 file changed, 36 insertions(+), 83 deletions(-) diff --git a/src/ts-protocol/client.ts b/src/ts-protocol/client.ts index bf27821..4284f76 100644 --- a/src/ts-protocol/client.ts +++ b/src/ts-protocol/client.ts @@ -73,10 +73,7 @@ export class TS3Client extends EventEmitter { private httpQuery: TS6HttpQuery | null = null; private udpErrorTimer: ReturnType | null = null; - constructor( - private options: TS3ClientOptions, - logger: Logger, - ) { + constructor(private options: TS3ClientOptions, logger: Logger) { super(); this.logger = logger; @@ -113,10 +110,7 @@ export class TS3Client extends EventEmitter { const addr = `${this.options.host}:${this.options.port}`; // Detect or use forced protocol - if ( - this.options.serverProtocol && - this.options.serverProtocol !== "unknown" - ) { + if (this.options.serverProtocol && this.options.serverProtocol !== "unknown") { this.detectedProtocol = this.options.serverProtocol; this.logger.info( { addr, protocol: this.detectedProtocol }, @@ -139,11 +133,7 @@ export class TS3Client extends EventEmitter { ); } else { this.logger.info( - { - addr, - protocol: this.detectedProtocol, - queryPort: detection.queryPort, - }, + { addr, protocol: this.detectedProtocol, queryPort: detection.queryPort }, `Server protocol detected: ${this.detectedProtocol.toUpperCase()}`, ); } @@ -151,8 +141,7 @@ export class TS3Client extends EventEmitter { // Set up TS6 HTTP Query if applicable if (this.detectedProtocol === "ts6") { - const queryPort = - this.options.queryPort !== 10011 ? this.options.queryPort : 10080; + const queryPort = this.options.queryPort !== 10011 ? this.options.queryPort : 10080; this.httpQuery = new TS6HttpQuery({ host: this.options.host, port: queryPort, @@ -163,9 +152,7 @@ export class TS3Client extends EventEmitter { // Guard against calling connect() while already connected. // Save detectedProtocol first because disconnect() resets it. if (this.client) { - this.logger.warn( - "connect() called while already connected, disconnecting first", - ); + this.logger.warn("connect() called while already connected, disconnecting first"); const savedProtocol = this.detectedProtocol; const savedHttpQuery = this.httpQuery; this.disconnect(); @@ -192,9 +179,7 @@ export class TS3Client extends EventEmitter { if (this.udpErrorTimer) clearTimeout(this.udpErrorTimer); this.udpErrorTimer = setTimeout(() => { if (udpErrorCount > 1) { - this.logger.warn( - `udp send error (repeated ${udpErrorCount} times, connection may be lost)`, - ); + this.logger.warn(`udp send error (repeated ${udpErrorCount} times, connection may be lost)`); } udpErrorCount = 0; this.udpErrorTimer = null; @@ -205,22 +190,17 @@ export class TS3Client extends EventEmitter { this.logger.warn(msg); }; - 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), - warn: throttledWarn, - error: (msg) => this.logger.error(msg), - }, + 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), + warn: throttledWarn, + error: (msg) => this.logger.error(msg), }, - ); + }); this.client.on("textMessage", (msg: TextMessage) => { const tsMsg: TS3TextMessage = { @@ -242,7 +222,7 @@ export class TS3Client extends EventEmitter { this.client.on("clientEnter", (info: ClientInfo) => { this.logger.debug( { nickname: info.nickname, id: info.id }, - "Client entered", + "Client entered" ); this.emit("clientEnter", info); }); @@ -255,7 +235,7 @@ export class TS3Client extends EventEmitter { this.client.on("clientMoved", (ev: ClientMovedEvent) => { this.logger.debug( { id: ev.id, targetChannelID: ev.targetChannelID.toString() }, - "Client moved", + "Client moved" ); this.emit("clientMoved", ev); }); @@ -277,14 +257,11 @@ export class TS3Client extends EventEmitter { // Join channel by numeric ID (takes precedence) or by name if (this.options.channelId) { - await this.joinChannel( - this.options.channelId, - this.options.channelPassword, - ); + await this.joinChannel(this.options.channelId, this.options.channelPassword); } else if (this.options.defaultChannel) { await this.joinChannel( this.options.defaultChannel, - this.options.channelPassword, + this.options.channelPassword ); } @@ -297,12 +274,7 @@ export class TS3Client extends EventEmitter { const isNumeric = /^\d+$/.test(channelName); if (isNumeric) { try { - await clientMove( - this.client, - this.clientId, - BigInt(channelName), - password, - ); + await clientMove(this.client, this.clientId, BigInt(channelName), password); } catch (err) { this.logger.error({ err, channelName }, "Failed to join channel"); } @@ -312,14 +284,16 @@ export class TS3Client extends EventEmitter { try { const channels = await listChannels(this.client); const channel = channels.find((ch) => ch.name === channelName); + if (!channel) { this.logger.warn({ channelName }, "Channel not found"); return; } + await clientMove(this.client, this.clientId, channel.id, password); this.logger.info( { channelName, cid: channel.id.toString() }, - "Joined channel", + "Joined channel" ); } catch (err) { this.logger.error({ err, channelName }, "Failed to join channel"); @@ -328,7 +302,7 @@ export class TS3Client extends EventEmitter { async sendTextMessage( message: string, - targetMode: number = 2, + targetMode: number = 2 ): Promise { if (!this.client) return; // targetMode 2 = channel, target 0 = current channel @@ -360,9 +334,7 @@ export class TS3Client extends EventEmitter { await this.client.sendCommandNoWait(cmd); } - async execCommandWithResponse( - cmd: string, - ): Promise[]> { + async execCommandWithResponse(cmd: string): Promise[]> { if (!this.client) throw new Error("Not connected"); return this.client.execCommandWithResponse(cmd); } @@ -375,28 +347,15 @@ export class TS3Client extends EventEmitter { overwrite = true, ): Promise { if (!this.client) throw new Error("Not connected"); - return this.client.fileTransferInitUpload( - channelID, - path, - password, - size, - overwrite, - ); + return this.client.fileTransferInitUpload(channelID, path, password, size, overwrite); } - async uploadFileData( - host: string, - info: FileUploadInfo, - data: Readable, - ): Promise { + async uploadFileData(host: string, info: FileUploadInfo, data: Readable): Promise { if (!this.client) throw new Error("Not connected"); await this.client.uploadFileData(host, info, data); } - async fileTransferDeleteFile( - channelID: bigint, - paths: string[], - ): Promise { + async fileTransferDeleteFile(channelID: bigint, paths: string[]): Promise { if (!this.client) throw new Error("Not connected"); await fileTransferDeleteFile(this.client, channelID, paths); } @@ -420,10 +379,7 @@ export class TS3Client extends EventEmitter { this.client.sendVoice(opusFrame, 5); this.voiceFramesSent++; if (this.voiceFramesSent === 1) { - this.logger.info( - { opusBytes: opusFrame.length, clientId: this.clientId }, - "First voice packet sent to TeamSpeak", - ); + this.logger.info({ opusBytes: opusFrame.length, clientId: this.clientId }, "First voice packet sent to TeamSpeak"); } } catch (err) { if (this.voiceFramesSent === 0) { @@ -444,15 +400,12 @@ export class TS3Client extends EventEmitter { if (this.client && !this.disconnecting) { this.disconnecting = true; const client = this.client; - client - .disconnect() - .catch(() => {}) - .finally(() => { - if (this.client === client) { - this.client = null; - } - this.disconnecting = false; - }); + client.disconnect().catch(() => {}).finally(() => { + if (this.client === client) { + this.client = null; + } + this.disconnecting = false; + }); } this.clientId = 0; this.httpQuery = null; From 62b5b0985701cb1d841ee98a1d00de9095da536e Mon Sep 17 00:00:00 2001 From: Dr1mH4X <102709153+Dr1mH4X@users.noreply.github.com> Date: Tue, 23 Jun 2026 02:45:09 +0800 Subject: [PATCH 3/3] chore: add success log for numeric channel join --- src/bot/manager.ts | 32 ++++++++++++++++---------------- src/ts-protocol/client.ts | 1 + web/src/views/Setup.vue | 2 +- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/bot/manager.ts b/src/bot/manager.ts index ebaf706..f9535d6 100644 --- a/src/bot/manager.ts +++ b/src/bot/manager.ts @@ -225,22 +225,22 @@ export class BotManager extends EventEmitter { const bot = new BotInstance({ id: saved.id, name: saved.name, - tsOptions: { - host: saved.serverAddress, - port: saved.serverPort, - queryPort: proto === "ts6" ? 10080 : 10011, - nickname: saved.nickname, - // Reuse the stored identity so server groups assigned to this bot - // survive restarts — without this the TS server sees a new UID - // each connect and strips all previously granted groups. - identity: saved.identity || undefined, - defaultChannel: saved.defaultChannel || undefined, - channelId: saved.channelId || undefined, - channelPassword: saved.channelPassword || undefined, - serverPassword: saved.serverPassword || undefined, - serverProtocol: proto === "ts3" || proto === "ts6" ? proto : undefined, - ts6ApiKey: saved.ts6ApiKey || undefined, - }, + tsOptions: { + host: saved.serverAddress, + port: saved.serverPort, + queryPort: proto === "ts6" ? 10080 : 10011, + nickname: saved.nickname, + // Reuse the stored identity so server groups assigned to this bot + // survive restarts — without this the TS server sees a new UID + // each connect and strips all previously granted groups. + identity: saved.identity || undefined, + defaultChannel: saved.defaultChannel || undefined, + channelId: saved.channelId || undefined, + channelPassword: saved.channelPassword || undefined, + serverPassword: saved.serverPassword || undefined, + serverProtocol: proto === "ts3" || proto === "ts6" ? proto : undefined, + ts6ApiKey: saved.ts6ApiKey || undefined, + }, neteaseProvider: this.neteaseProvider, qqProvider: this.qqProvider, bilibiliProvider: this.bilibiliProvider, diff --git a/src/ts-protocol/client.ts b/src/ts-protocol/client.ts index 4284f76..f743421 100644 --- a/src/ts-protocol/client.ts +++ b/src/ts-protocol/client.ts @@ -275,6 +275,7 @@ export class TS3Client extends EventEmitter { if (isNumeric) { try { await clientMove(this.client, this.clientId, BigInt(channelName), password); + this.logger.info({ channelName }, "Joined channel"); } catch (err) { this.logger.error({ err, channelName }, "Failed to join channel"); } diff --git a/web/src/views/Setup.vue b/web/src/views/Setup.vue index 6bf2d23..1a7bb14 100644 --- a/web/src/views/Setup.vue +++ b/web/src/views/Setup.vue @@ -51,7 +51,7 @@
- +