feat: Add channelId support to bot configuration and database

This commit is contained in:
Dr1mH4X committed 2026-06-23 02:37:13 +08:00
1 parent 6f21b6354a
commit 4244695075
7 files changed
+168 -67

No files matched your search

+21 -15
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,
@@ -221,21 +225,22 @@ export class BotManager extends EventEmitter {
const bot = new BotInstance({ const bot = new BotInstance({
id: saved.id, id: saved.id,
name: saved.name, name: saved.name,
tsOptions: { tsOptions: {
host: saved.serverAddress, host: saved.serverAddress,
port: saved.serverPort, port: saved.serverPort,
queryPort: proto === "ts6" ? 10080 : 10011, queryPort: proto === "ts6" ? 10080 : 10011,
nickname: saved.nickname, nickname: saved.nickname,
// Reuse the stored identity so server groups assigned to this bot // Reuse the stored identity so server groups assigned to this bot
// survive restarts — without this the TS server sees a new UID // survive restarts — without this the TS server sees a new UID
// 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,
channelPassword: saved.channelPassword || undefined, channelId: saved.channelId || undefined,
serverPassword: saved.serverPassword || undefined, channelPassword: saved.channelPassword || undefined,
serverProtocol: proto === "ts3" || proto === "ts6" ? proto : undefined, serverPassword: saved.serverPassword || undefined,
ts6ApiKey: saved.ts6ApiKey || undefined, serverProtocol: proto === "ts3" || proto === "ts6" ? proto : undefined,
}, ts6ApiKey: saved.ts6ApiKey || undefined,
},
neteaseProvider: this.neteaseProvider, neteaseProvider: this.neteaseProvider,
qqProvider: this.qqProvider, qqProvider: this.qqProvider,
bilibiliProvider: this.bilibiliProvider, bilibiliProvider: this.bilibiliProvider,
@@ -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,
})); }));
}, },
+97 -42
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. */
@@ -72,7 +73,10 @@ export class TS3Client extends EventEmitter {
private httpQuery: TS6HttpQuery | null = null; private httpQuery: TS6HttpQuery | null = null;
private udpErrorTimer: ReturnType<typeof setTimeout> | null = null; private udpErrorTimer: ReturnType<typeof setTimeout> | null = null;
constructor(private options: TS3ClientOptions, logger: Logger) { constructor(
private options: TS3ClientOptions,
logger: Logger,
) {
super(); super();
this.logger = logger; this.logger = logger;
@@ -109,7 +113,10 @@ export class TS3Client extends EventEmitter {
const addr = `${this.options.host}:${this.options.port}`; const addr = `${this.options.host}:${this.options.port}`;
// Detect or use forced protocol // 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.detectedProtocol = this.options.serverProtocol;
this.logger.info( this.logger.info(
{ addr, protocol: this.detectedProtocol }, { addr, protocol: this.detectedProtocol },
@@ -132,7 +139,11 @@ export class TS3Client extends EventEmitter {
); );
} else { } else {
this.logger.info( this.logger.info(
{ addr, protocol: this.detectedProtocol, queryPort: detection.queryPort }, {
addr,
protocol: this.detectedProtocol,
queryPort: detection.queryPort,
},
`Server protocol detected: ${this.detectedProtocol.toUpperCase()}`, `Server protocol detected: ${this.detectedProtocol.toUpperCase()}`,
); );
} }
@@ -140,7 +151,8 @@ export class TS3Client extends EventEmitter {
// Set up TS6 HTTP Query if applicable // Set up TS6 HTTP Query if applicable
if (this.detectedProtocol === "ts6") { 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({ this.httpQuery = new TS6HttpQuery({
host: this.options.host, host: this.options.host,
port: queryPort, port: queryPort,
@@ -151,7 +163,9 @@ export class TS3Client extends EventEmitter {
// Guard against calling connect() while already connected. // Guard against calling connect() while already connected.
// Save detectedProtocol first because disconnect() resets it. // Save detectedProtocol first because disconnect() resets it.
if (this.client) { 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 savedProtocol = this.detectedProtocol;
const savedHttpQuery = this.httpQuery; const savedHttpQuery = this.httpQuery;
this.disconnect(); this.disconnect();
@@ -178,7 +192,9 @@ export class TS3Client extends EventEmitter {
if (this.udpErrorTimer) clearTimeout(this.udpErrorTimer); if (this.udpErrorTimer) clearTimeout(this.udpErrorTimer);
this.udpErrorTimer = setTimeout(() => { this.udpErrorTimer = setTimeout(() => {
if (udpErrorCount > 1) { 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; udpErrorCount = 0;
this.udpErrorTimer = null; this.udpErrorTimer = null;
@@ -189,17 +205,22 @@ export class TS3Client extends EventEmitter {
this.logger.warn(msg); this.logger.warn(msg);
}; };
this.client = new TS3FullClient(this.identity, addr, this.options.nickname, { this.client = new TS3FullClient(
// Forward server password to the protocol library so it can be this.identity,
// included in clientinit for password-protected servers addr,
serverPassword: this.options.serverPassword, this.options.nickname,
logger: { {
debug: (msg) => this.logger.debug(msg), // Forward server password to the protocol library so it can be
info: (msg) => this.logger.info(msg), // included in clientinit for password-protected servers
warn: throttledWarn, serverPassword: this.options.serverPassword,
error: (msg) => this.logger.error(msg), 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) => { this.client.on("textMessage", (msg: TextMessage) => {
const tsMsg: TS3TextMessage = { const tsMsg: TS3TextMessage = {
@@ -221,7 +242,7 @@ export class TS3Client extends EventEmitter {
this.client.on("clientEnter", (info: ClientInfo) => { this.client.on("clientEnter", (info: ClientInfo) => {
this.logger.debug( this.logger.debug(
{ nickname: info.nickname, id: info.id }, { nickname: info.nickname, id: info.id },
"Client entered" "Client entered",
); );
this.emit("clientEnter", info); this.emit("clientEnter", info);
}); });
@@ -234,7 +255,7 @@ export class TS3Client extends EventEmitter {
this.client.on("clientMoved", (ev: ClientMovedEvent) => { this.client.on("clientMoved", (ev: ClientMovedEvent) => {
this.logger.debug( this.logger.debug(
{ id: ev.id, targetChannelID: ev.targetChannelID.toString() }, { id: ev.id, targetChannelID: ev.targetChannelID.toString() },
"Client moved" "Client moved",
); );
this.emit("clientMoved", ev); this.emit("clientMoved", ev);
}); });
@@ -254,11 +275,16 @@ 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,24 +294,32 @@ 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,
);
} 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);
if (!channel) { if (!channel) {
this.logger.warn({ channelName }, "Channel not found"); this.logger.warn({ channelName }, "Channel not found");
return; return;
} }
await clientMove(this.client, this.clientId, channel.id, password);
await clientMove(
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",
); );
} catch (err) { } catch (err) {
this.logger.error({ err, channelName }, "Failed to join channel"); this.logger.error({ err, channelName }, "Failed to join channel");
@@ -294,7 +328,7 @@ export class TS3Client extends EventEmitter {
async sendTextMessage( async sendTextMessage(
message: string, message: string,
targetMode: number = 2 targetMode: number = 2,
): Promise<void> { ): Promise<void> {
if (!this.client) return; if (!this.client) return;
// targetMode 2 = channel, target 0 = current channel // targetMode 2 = channel, target 0 = current channel
@@ -326,7 +360,9 @@ export class TS3Client extends EventEmitter {
await this.client.sendCommandNoWait(cmd); await this.client.sendCommandNoWait(cmd);
} }
async execCommandWithResponse(cmd: string): Promise<Record<string, string>[]> { async execCommandWithResponse(
cmd: string,
): Promise<Record<string, string>[]> {
if (!this.client) throw new Error("Not connected"); if (!this.client) throw new Error("Not connected");
return this.client.execCommandWithResponse(cmd); return this.client.execCommandWithResponse(cmd);
} }
@@ -339,15 +375,28 @@ export class TS3Client extends EventEmitter {
overwrite = true, overwrite = true,
): Promise<FileUploadInfo> { ): Promise<FileUploadInfo> {
if (!this.client) throw new Error("Not connected"); 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<void> { async uploadFileData(
host: string,
info: FileUploadInfo,
data: Readable,
): Promise<void> {
if (!this.client) throw new Error("Not connected"); if (!this.client) throw new Error("Not connected");
await this.client.uploadFileData(host, info, data); await this.client.uploadFileData(host, info, data);
} }
async fileTransferDeleteFile(channelID: bigint, paths: string[]): Promise<void> { async fileTransferDeleteFile(
channelID: bigint,
paths: string[],
): Promise<void> {
if (!this.client) throw new Error("Not connected"); if (!this.client) throw new Error("Not connected");
await fileTransferDeleteFile(this.client, channelID, paths); await fileTransferDeleteFile(this.client, channelID, paths);
} }
@@ -371,7 +420,10 @@ export class TS3Client extends EventEmitter {
this.client.sendVoice(opusFrame, 5); this.client.sendVoice(opusFrame, 5);
this.voiceFramesSent++; this.voiceFramesSent++;
if (this.voiceFramesSent === 1) { 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) { } catch (err) {
if (this.voiceFramesSent === 0) { if (this.voiceFramesSent === 0) {
@@ -392,12 +444,15 @@ export class TS3Client extends EventEmitter {
if (this.client && !this.disconnecting) { if (this.client && !this.disconnecting) {
this.disconnecting = true; this.disconnecting = true;
const client = this.client; const client = this.client;
client.disconnect().catch(() => {}).finally(() => { client
if (this.client === client) { .disconnect()
this.client = null; .catch(() => {})
} .finally(() => {
this.disconnecting = false; if (this.client === client) {
}); this.client = null;
}
this.disconnecting = false;
});
} }
this.clientId = 0; this.clientId = 0;
this.httpQuery = null; this.httpQuery = null;
+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) {
+22 -4
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>
@@ -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="如 1234" 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 {