feat: implement server password login, YouTube source, and improved bot selector UI

- **TS server password** (#7/#9): add serverPassword field across database,
  manager, bot API, and Settings UI — allows joining password-protected servers
- **YouTube audio source** (#1/#10): new YouTubeProvider using yt-dlp binary;
  adds -y flag in chat commands, /api/music supports platform=youtube,
  YouTube badge in SongCard, yt-dlp-wrap npm dependency
- **Bot selector UI** (#14): selector always visible (not just when >1 bot),
  bigger button with border and play-state indicator; per-bot URL routing at
  /bot/:id with BotRedirect view; copy-link button in dropdown

Closes #1, #7, #9, #10, #14

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Sonnet 4.6 committed 2026-04-09 11:58:07 +08:00
1 parent b20ddf2603
commit d5d6abeb1e
16 files changed
+391 -40

No files matched your search

+42
View File
@@ -0,0 +1,42 @@
<template>
<div class="redirect-page">
<span v-if="notFound">机器人不存在或未加载</span>
<span v-else>正在跳转...</span>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { usePlayerStore } from '../stores/player.js';
const route = useRoute();
const router = useRouter();
const store = usePlayerStore();
const notFound = ref(false);
onMounted(async () => {
const botId = route.params.id as string;
if (!store.bots.length) {
await store.fetchBots();
}
const bot = store.bots.find((b) => b.id === botId);
if (bot) {
store.setActiveBotId(botId);
router.replace('/');
} else {
notFound.value = true;
}
});
</script>
<style scoped>
.redirect-page {
display: flex;
align-items: center;
justify-content: center;
height: 60vh;
font-size: 16px;
color: var(--text-secondary);
}
</style>
+14
View File
@@ -76,6 +76,10 @@
<label>频道密码(可选)</label>
<input v-model="editForm.channelPassword" class="input" type="password" />
</div>
<div class="form-group">
<label>服务器密码(可选)</label>
<input v-model="editForm.serverPassword" class="input" type="password" placeholder="服务器有密码时填写" />
</div>
<div class="modal-actions">
<button class="btn-secondary" @click="editingBot = null">取消</button>
<button class="btn-primary" @click="saveEditBot">保存(需重启机器人生效)</button>
@@ -108,6 +112,10 @@
<label>默认频道(可选)</label>
<input v-model="newBotChannel" class="input" placeholder="音乐频道" />
</div>
<div class="form-group">
<label>服务器密码(可选)</label>
<input v-model="newBotServerPassword" class="input" type="password" placeholder="服务器有密码时填写" />
</div>
<button class="btn-primary" @click="createBot">创建</button>
</div>
</section>
@@ -394,6 +402,7 @@ const newBotServer = ref('');
const newBotPort = ref(9987);
const newBotNickname = ref('MusicBot');
const newBotChannel = ref('');
const newBotServerPassword = ref('');
// Edit bot
const editingBot = ref<string | null>(null);
@@ -404,6 +413,7 @@ const editForm = reactive({
nickname: '',
defaultChannel: '',
channelPassword: '',
serverPassword: '',
});
const neteaseCookie = ref('');
@@ -559,6 +569,7 @@ async function createBot() {
serverPort: newBotPort.value || 9987,
nickname: newBotNickname.value || newBotName.value,
defaultChannel: newBotChannel.value || undefined,
serverPassword: newBotServerPassword.value || undefined,
autoStart: false,
});
newBotName.value = '';
@@ -566,6 +577,7 @@ async function createBot() {
newBotPort.value = 9987;
newBotNickname.value = 'MusicBot';
newBotChannel.value = '';
newBotServerPassword.value = '';
await store.fetchBots();
} catch {
// Ignore
@@ -598,6 +610,7 @@ async function openEditBot(bot: any) {
editForm.nickname = res.data.nickname ?? '';
editForm.defaultChannel = res.data.defaultChannel ?? '';
editForm.channelPassword = res.data.channelPassword ?? '';
editForm.serverPassword = res.data.serverPassword ?? '';
} catch {
// Config not found — use defaults
editForm.serverAddress = '';
@@ -605,6 +618,7 @@ async function openEditBot(bot: any) {
editForm.nickname = bot.name;
editForm.defaultChannel = '';
editForm.channelPassword = '';
editForm.serverPassword = '';
}
}