Merge pull request #100 from Dr1mH4X/feat/channelid

feat: joinChannel via channelid
This commit is contained in:
TIANYAO ZHANG authored and GitHub committed 2026-06-24 22:58:45 +08:00
commit 06aaec4ba5
7 files changed
+74 -19

No files matched your search

+6
View File
@@ -57,6 +57,7 @@ export interface CreateBotParams {
queryPort?: number; queryPort?: number;
nickname: string; nickname: string;
defaultChannel?: string; defaultChannel?: string;
channelId?: string;
channelPassword?: string; channelPassword?: string;
autoStart?: boolean; autoStart?: boolean;
/** Force TS3 or TS6 protocol; omit or "unknown" for auto-detect. */ /** Force TS3 or TS6 protocol; omit or "unknown" for auto-detect. */
@@ -113,6 +114,7 @@ export class BotManager extends EventEmitter {
queryPort: params.queryPort ?? 10011, queryPort: params.queryPort ?? 10011,
nickname: params.nickname, nickname: params.nickname,
defaultChannel: params.defaultChannel, defaultChannel: params.defaultChannel,
channelId: params.channelId,
channelPassword: params.channelPassword, channelPassword: params.channelPassword,
serverPassword: params.serverPassword, serverPassword: params.serverPassword,
serverProtocol: params.serverProtocol, serverProtocol: params.serverProtocol,
@@ -138,6 +140,7 @@ export class BotManager extends EventEmitter {
serverPort: params.serverPort, serverPort: params.serverPort,
nickname: params.nickname, nickname: params.nickname,
defaultChannel: params.defaultChannel ?? "", defaultChannel: params.defaultChannel ?? "",
channelId: params.channelId ?? "",
channelPassword: params.channelPassword ?? "", channelPassword: params.channelPassword ?? "",
autoStart: params.autoStart ?? false, autoStart: params.autoStart ?? false,
serverProtocol: params.serverProtocol ?? "", serverProtocol: params.serverProtocol ?? "",
@@ -173,6 +176,7 @@ export class BotManager extends EventEmitter {
serverPort: params.serverPort ?? existing.serverPort, serverPort: params.serverPort ?? existing.serverPort,
nickname: params.nickname ?? existing.nickname, nickname: params.nickname ?? existing.nickname,
defaultChannel: params.defaultChannel ?? existing.defaultChannel, defaultChannel: params.defaultChannel ?? existing.defaultChannel,
channelId: params.channelId ?? existing.channelId,
channelPassword: params.channelPassword ?? existing.channelPassword, channelPassword: params.channelPassword ?? existing.channelPassword,
serverProtocol: params.serverProtocol ?? existing.serverProtocol, serverProtocol: params.serverProtocol ?? existing.serverProtocol,
ts6ApiKey: params.ts6ApiKey ?? existing.ts6ApiKey, ts6ApiKey: params.ts6ApiKey ?? existing.ts6ApiKey,
@@ -231,6 +235,7 @@ export class BotManager extends EventEmitter {
// each connect and strips all previously granted groups. // each connect and strips all previously granted groups.
identity: saved.identity || undefined, identity: saved.identity || undefined,
defaultChannel: saved.defaultChannel || undefined, defaultChannel: saved.defaultChannel || undefined,
channelId: saved.channelId || undefined,
channelPassword: saved.channelPassword || undefined, channelPassword: saved.channelPassword || undefined,
serverPassword: saved.serverPassword || undefined, serverPassword: saved.serverPassword || undefined,
serverProtocol: proto === "ts3" || proto === "ts6" ? proto : undefined, serverProtocol: proto === "ts3" || proto === "ts6" ? proto : undefined,
@@ -282,6 +287,7 @@ export class BotManager extends EventEmitter {
nickname: saved.nickname, nickname: saved.nickname,
identity: saved.identity || undefined, identity: saved.identity || undefined,
defaultChannel: saved.defaultChannel || undefined, defaultChannel: saved.defaultChannel || undefined,
channelId: saved.channelId || undefined,
channelPassword: saved.channelPassword || undefined, channelPassword: saved.channelPassword || undefined,
serverPassword: saved.serverPassword || undefined, serverPassword: saved.serverPassword || undefined,
serverProtocol: proto === "ts3" || proto === "ts6" ? proto : undefined, serverProtocol: proto === "ts3" || proto === "ts6" ? proto : undefined,
+3
View File
@@ -82,6 +82,7 @@ describe("database", () => {
serverPort: 9987, serverPort: 9987,
nickname: "MusicBot", nickname: "MusicBot",
defaultChannel: "Music", defaultChannel: "Music",
channelId: "",
channelPassword: "", channelPassword: "",
autoStart: true, autoStart: true,
serverProtocol: "", serverProtocol: "",
@@ -111,6 +112,7 @@ describe("database", () => {
serverPort: 9987, serverPort: 9987,
nickname: "MusicBot", nickname: "MusicBot",
defaultChannel: "Music", defaultChannel: "Music",
channelId: "",
channelPassword: "", channelPassword: "",
autoStart: false, autoStart: false,
serverProtocol: "", serverProtocol: "",
@@ -131,6 +133,7 @@ describe("database", () => {
serverPort: 9987, serverPort: 9987,
nickname: "n", nickname: "n",
defaultChannel: "", defaultChannel: "",
channelId: "",
channelPassword: "", channelPassword: "",
autoStart: false, autoStart: false,
serverProtocol: "", serverProtocol: "",
+9 -2
View File
@@ -23,6 +23,7 @@ export interface BotInstance {
serverPort: number; serverPort: number;
nickname: string; nickname: string;
defaultChannel: string; defaultChannel: string;
channelId: string;
channelPassword: string; channelPassword: string;
autoStart: boolean; autoStart: boolean;
/** "ts3" | "ts6" | "" (empty = auto-detect) */ /** "ts3" | "ts6" | "" (empty = auto-detect) */
@@ -96,6 +97,9 @@ function migrateSchema(db: Database.Database): void {
if (!names.includes("serverPassword")) { if (!names.includes("serverPassword")) {
db.exec("ALTER TABLE bot_instances ADD COLUMN serverPassword TEXT NOT NULL DEFAULT ''"); 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 // Profile feature flags
const profileCols = [ const profileCols = [
"profile_avatar_enabled", "profile_avatar_enabled",
@@ -142,6 +146,7 @@ function initTables(db: Database.Database): void {
serverPort INTEGER NOT NULL, serverPort INTEGER NOT NULL,
nickname TEXT NOT NULL, nickname TEXT NOT NULL,
defaultChannel TEXT NOT NULL, defaultChannel TEXT NOT NULL,
channelId TEXT NOT NULL DEFAULT '',
channelPassword TEXT NOT NULL, channelPassword TEXT NOT NULL,
autoStart INTEGER NOT NULL DEFAULT 0, autoStart INTEGER NOT NULL DEFAULT 0,
serverProtocol TEXT NOT NULL DEFAULT '', serverProtocol TEXT NOT NULL DEFAULT '',
@@ -254,14 +259,15 @@ export function createDatabase(dbPath: string): BotDatabase {
`); `);
const upsertInstance = db.prepare(` const upsertInstance = db.prepare(`
INSERT INTO bot_instances (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, @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 ON CONFLICT(id) DO UPDATE SET
name = excluded.name, name = excluded.name,
serverAddress = excluded.serverAddress, serverAddress = excluded.serverAddress,
serverPort = excluded.serverPort, serverPort = excluded.serverPort,
nickname = excluded.nickname, nickname = excluded.nickname,
defaultChannel = excluded.defaultChannel, defaultChannel = excluded.defaultChannel,
channelId = excluded.channelId,
channelPassword = excluded.channelPassword, channelPassword = excluded.channelPassword,
autoStart = excluded.autoStart, autoStart = excluded.autoStart,
serverProtocol = excluded.serverProtocol, serverProtocol = excluded.serverProtocol,
@@ -342,6 +348,7 @@ export function createDatabase(dbPath: string): BotDatabase {
serverProtocol: r.serverProtocol ?? "", serverProtocol: r.serverProtocol ?? "",
ts6ApiKey: r.ts6ApiKey ?? "", ts6ApiKey: r.ts6ApiKey ?? "",
serverPassword: r.serverPassword ?? "", serverPassword: r.serverPassword ?? "",
channelId: r.channelId ?? "",
identity: r.identity ?? undefined, identity: r.identity ?? undefined,
})); }));
}, },
+17 -8
View File
@@ -46,6 +46,7 @@ export interface TS3ClientOptions {
nickname: string; nickname: string;
identity?: string; // Exported identity string, or undefined to generate new identity?: string; // Exported identity string, or undefined to generate new
defaultChannel?: string; defaultChannel?: string;
channelId?: string; // Numeric channel ID (takes precedence over defaultChannel)
channelPassword?: string; channelPassword?: string;
serverPassword?: string; serverPassword?: string;
/** Force a specific protocol instead of auto-detecting. */ /** 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)`, `Logged in (visible client, ${this.detectedProtocol.toUpperCase()} server)`,
); );
// Join default channel if specified // Join channel by numeric ID (takes precedence) or by name
if (this.options.defaultChannel) { if (this.options.channelId) {
await this.joinChannel(this.options.channelId, this.options.channelPassword);
} else if (this.options.defaultChannel) {
await this.joinChannel( await this.joinChannel(
this.options.defaultChannel, this.options.defaultChannel,
this.options.channelPassword this.options.channelPassword
@@ -268,6 +271,17 @@ export class TS3Client extends EventEmitter {
async joinChannel(channelName: string, password?: string): Promise<void> { async joinChannel(channelName: string, password?: string): Promise<void> {
if (!this.client) return; 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 { try {
const channels = await listChannels(this.client); const channels = await listChannels(this.client);
const channel = channels.find((ch) => ch.name === channelName); const channel = channels.find((ch) => ch.name === channelName);
@@ -277,12 +291,7 @@ export class TS3Client extends EventEmitter {
return; return;
} }
await clientMove( await clientMove(this.client, this.clientId, channel.id, password);
this.client,
this.clientId,
channel.id,
password
);
this.logger.info( this.logger.info(
{ channelName, cid: channel.id.toString() }, { channelName, cid: channel.id.toString() },
"Joined channel" "Joined channel"
+4 -2
View File
@@ -159,6 +159,7 @@ export function createBotRouter(
serverPort, serverPort,
nickname, nickname,
defaultChannel, defaultChannel,
channelId,
channelPassword, channelPassword,
serverPassword, serverPassword,
autoStart, autoStart,
@@ -175,6 +176,7 @@ export function createBotRouter(
serverPort: serverPort ?? 9987, serverPort: serverPort ?? 9987,
nickname, nickname,
defaultChannel, defaultChannel,
channelId,
channelPassword, channelPassword,
serverPassword, serverPassword,
autoStart: autoStart ?? false, autoStart: autoStart ?? false,
@@ -194,10 +196,10 @@ export function createBotRouter(
res.status(404).json({ error: "Bot not found" }); res.status(404).json({ error: "Bot not found" });
return; 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 // Update in database
botManager.updateBot(req.params.id, { botManager.updateBot(req.params.id, {
name, serverAddress, serverPort, nickname, defaultChannel, channelPassword, serverPassword, name, serverAddress, serverPort, nickname, defaultChannel, channelId, channelPassword, serverPassword,
}); });
res.json({ success: true }); res.json({ success: true });
} catch (err) { } catch (err) {
+23 -5
View File
@@ -98,8 +98,12 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>默认频道(可选)</label> <label>默认频道名称(可选)</label>
<input v-model="editForm.defaultChannel" class="input" placeholder="音乐频道" /> <input v-model="editForm.defaultChannel" :disabled="!!editForm.channelId" class="input" :class="{ disabled: !!editForm.channelId }" placeholder="音乐频道" />
</div>
<div class="form-group">
<label>默认频道ID(可选)</label>
<input v-model="editForm.channelId" :disabled="!!editForm.defaultChannel" class="input" :class="{ disabled: !!editForm.defaultChannel }" placeholder="如 12" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label>频道密码(可选)</label> <label>频道密码(可选)</label>
@@ -142,8 +146,12 @@
<input v-model="newBotNickname" class="input" placeholder="MusicBot" /> <input v-model="newBotNickname" class="input" placeholder="MusicBot" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label>默认频道(可选)</label> <label>默认频道名称(可选)</label>
<input v-model="newBotChannel" class="input" placeholder="音乐频道" /> <input v-model="newBotChannel" :disabled="!!newBotChannelId" class="input" :class="{ disabled: !!newBotChannelId }" placeholder="音乐频道" />
</div>
<div class="form-group">
<label>默认频道ID(可选)</label>
<input v-model="newBotChannelId" :disabled="!!newBotChannel" class="input" :class="{ disabled: !!newBotChannel }" placeholder="如 12" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label>服务器密码(可选)</label> <label>服务器密码(可选)</label>
@@ -408,7 +416,7 @@
</div> </div>
</div> </div>
</section> </section>
<!-- Idle Timeout --> <!-- Idle Timeout -->
<section v-if="can('bot.manage')" class="settings-section"> <section v-if="can('bot.manage')" class="settings-section">
<h2 class="section-title">行为设置</h2> <h2 class="section-title">行为设置</h2>
@@ -685,6 +693,7 @@ const newBotServer = ref('');
const newBotPort = ref(9987); const newBotPort = ref(9987);
const newBotNickname = ref('MusicBot'); const newBotNickname = ref('MusicBot');
const newBotChannel = ref(''); const newBotChannel = ref('');
const newBotChannelId = ref('');
const newBotServerPassword = ref(''); const newBotServerPassword = ref('');
const newBotAvatar = ref<string | null>(null); const newBotAvatar = ref<string | null>(null);
@@ -696,6 +705,7 @@ const editForm = reactive({
serverPort: 9987, serverPort: 9987,
nickname: '', nickname: '',
defaultChannel: '', defaultChannel: '',
channelId: '',
channelPassword: '', channelPassword: '',
serverPassword: '', serverPassword: '',
}); });
@@ -853,6 +863,7 @@ async function createBot() {
serverPort: newBotPort.value || 9987, serverPort: newBotPort.value || 9987,
nickname: newBotNickname.value || newBotName.value, nickname: newBotNickname.value || newBotName.value,
defaultChannel: newBotChannel.value || undefined, defaultChannel: newBotChannel.value || undefined,
channelId: newBotChannelId.value || undefined,
serverPassword: newBotServerPassword.value || undefined, serverPassword: newBotServerPassword.value || undefined,
autoStart: false, autoStart: false,
}); });
@@ -868,6 +879,7 @@ async function createBot() {
newBotPort.value = 9987; newBotPort.value = 9987;
newBotNickname.value = 'MusicBot'; newBotNickname.value = 'MusicBot';
newBotChannel.value = ''; newBotChannel.value = '';
newBotChannelId.value = '';
newBotServerPassword.value = ''; newBotServerPassword.value = '';
newBotAvatar.value = null; newBotAvatar.value = null;
await store.fetchBots(); await store.fetchBots();
@@ -901,6 +913,7 @@ async function openEditBot(bot: any) {
editForm.serverPort = res.data.serverPort ?? 9987; editForm.serverPort = res.data.serverPort ?? 9987;
editForm.nickname = res.data.nickname ?? ''; editForm.nickname = res.data.nickname ?? '';
editForm.defaultChannel = res.data.defaultChannel ?? ''; editForm.defaultChannel = res.data.defaultChannel ?? '';
editForm.channelId = res.data.channelId ?? '';
editForm.channelPassword = res.data.channelPassword ?? ''; editForm.channelPassword = res.data.channelPassword ?? '';
editForm.serverPassword = res.data.serverPassword ?? ''; editForm.serverPassword = res.data.serverPassword ?? '';
} catch { } catch {
@@ -909,6 +922,7 @@ async function openEditBot(bot: any) {
editForm.serverPort = 9987; editForm.serverPort = 9987;
editForm.nickname = bot.name; editForm.nickname = bot.name;
editForm.defaultChannel = ''; editForm.defaultChannel = '';
editForm.channelId = '';
editForm.channelPassword = ''; editForm.channelPassword = '';
editForm.serverPassword = ''; editForm.serverPassword = '';
} }
@@ -1635,6 +1649,10 @@ onUnmounted(() => {
font-size: 13px; font-size: 13px;
outline: none; outline: none;
&:focus { border-color: var(--color-primary); } &:focus { border-color: var(--color-primary); }
&.disabled {
opacity: 0.4;
cursor: not-allowed;
}
} }
.input-sm { max-width: 80px; } .input-sm { max-width: 80px; }
+12 -2
View File
@@ -46,8 +46,12 @@
<input v-model="nickname" placeholder="MusicBot" class="input" /> <input v-model="nickname" placeholder="MusicBot" class="input" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label>默认频道 (可选)</label> <label>默认频道名称(可选)</label>
<input v-model="defaultChannel" placeholder="音乐频道" class="input" /> <input v-model="defaultChannel" :disabled="!!channelId" placeholder="音乐频道" class="input" :class="{ disabled: !!channelId }" />
</div>
<div class="form-group">
<label>默认频道ID(可选)</label>
<input v-model="channelId" :disabled="!!defaultChannel" placeholder="如 12" class="input" :class="{ disabled: !!defaultChannel }" />
</div> </div>
<div class="btn-row"> <div class="btn-row">
<button class="btn-secondary" @click="currentStep = 0">上一步</button> <button class="btn-secondary" @click="currentStep = 0">上一步</button>
@@ -87,6 +91,7 @@ const serverAddress = ref('');
const serverPort = ref(9987); const serverPort = ref(9987);
const nickname = ref('MusicBot'); const nickname = ref('MusicBot');
const defaultChannel = ref(''); const defaultChannel = ref('');
const channelId = ref('');
async function createBotAndNext() { async function createBotAndNext() {
try { try {
@@ -96,6 +101,7 @@ async function createBotAndNext() {
serverPort: serverPort.value, serverPort: serverPort.value,
nickname: nickname.value, nickname: nickname.value,
defaultChannel: defaultChannel.value, defaultChannel: defaultChannel.value,
channelId: channelId.value || undefined,
autoStart: true, autoStart: true,
}); });
currentStep.value = 2; currentStep.value = 2;
@@ -193,6 +199,10 @@ async function createBotAndNext() {
&:focus { &:focus {
border-color: var(--color-primary); border-color: var(--color-primary);
} }
&.disabled {
opacity: 0.4;
cursor: not-allowed;
}
} }
.btn-primary { .btn-primary {