diff --git a/src/bot/manager.ts b/src/bot/manager.ts index 5748da1..f9535d6 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, @@ -231,6 +235,7 @@ export class BotManager extends EventEmitter { // 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, @@ -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..f743421 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. */ @@ -254,8 +255,10 @@ 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 @@ -268,6 +271,17 @@ 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); + this.logger.info({ channelName }, "Joined channel"); + } 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); @@ -277,12 +291,7 @@ export class TS3Client extends EventEmitter { 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" 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..1a7bb14 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 {