mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-01 20:42:50 +08:00
feat: 机器人管理 — 删除、编辑配置、完整创建表单
- PUT /api/bot/:id 更新机器人配置(保存到数据库,重启生效) - GET /api/bot/:id/config 获取保存的配置 - BotManager.updateBot/getBotConfig 方法 - Settings 页面:删除按钮(带确认)、编辑弹窗(修改所有字段) - 创建表单:完整字段(名称、地址、端口、昵称、默认频道) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
bc9b2366d0
commit
067a30233f
3 files changed
+250
-6
No files matched your search
@@ -93,6 +93,27 @@ export class BotManager {
|
|||||||
this.logger.info({ botId: id }, "Bot instance removed");
|
this.logger.info({ botId: id }, "Bot instance removed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateBot(id: string, params: Partial<CreateBotParams>): void {
|
||||||
|
const instances = this.database.getBotInstances();
|
||||||
|
const existing = instances.find((i) => i.id === id);
|
||||||
|
if (!existing) throw new Error(`Bot ${id} not found`);
|
||||||
|
|
||||||
|
this.database.saveBotInstance({
|
||||||
|
...existing,
|
||||||
|
name: params.name ?? existing.name,
|
||||||
|
serverAddress: params.serverAddress ?? existing.serverAddress,
|
||||||
|
serverPort: params.serverPort ?? existing.serverPort,
|
||||||
|
nickname: params.nickname ?? existing.nickname,
|
||||||
|
defaultChannel: params.defaultChannel ?? existing.defaultChannel,
|
||||||
|
channelPassword: params.channelPassword ?? existing.channelPassword,
|
||||||
|
});
|
||||||
|
this.logger.info({ botId: id }, "Bot instance config updated (restart to apply)");
|
||||||
|
}
|
||||||
|
|
||||||
|
getBotConfig(id: string): import("../data/database.js").BotInstance | undefined {
|
||||||
|
return this.database.getBotInstances().find((i) => i.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
getBot(id: string): BotInstance | undefined {
|
getBot(id: string): BotInstance | undefined {
|
||||||
return this.bots.get(id);
|
return this.bots.get(id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,16 @@ export function createBotRouter(
|
|||||||
res.json(bot.getStatus());
|
res.json(bot.getStatus());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get saved config for a bot
|
||||||
|
router.get("/:id/config", (req, res) => {
|
||||||
|
const saved = botManager.getBotConfig(req.params.id);
|
||||||
|
if (!saved) {
|
||||||
|
res.status(404).json({ error: "Bot config not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json(saved);
|
||||||
|
});
|
||||||
|
|
||||||
router.post("/", async (req, res) => {
|
router.post("/", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
@@ -57,6 +67,26 @@ export function createBotRouter(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update bot config (must be stopped first to apply connection changes)
|
||||||
|
router.put("/:id", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const bot = botManager.getBot(req.params.id);
|
||||||
|
if (!bot) {
|
||||||
|
res.status(404).json({ error: "Bot not found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { name, serverAddress, serverPort, nickname, defaultChannel, channelPassword } = req.body;
|
||||||
|
// Update in database
|
||||||
|
botManager.updateBot(req.params.id, {
|
||||||
|
name, serverAddress, serverPort, nickname, defaultChannel, channelPassword,
|
||||||
|
});
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({ err }, "Failed to update bot");
|
||||||
|
res.status(500).json({ error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.delete("/:id", async (req, res) => {
|
router.delete("/:id", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await botManager.removeBot(req.params.id);
|
await botManager.removeBot(req.params.id);
|
||||||
|
|||||||
+199
-6
@@ -36,17 +36,79 @@
|
|||||||
<button class="btn-sm" @click="toggleBot(bot.id, bot.connected)">
|
<button class="btn-sm" @click="toggleBot(bot.id, bot.connected)">
|
||||||
{{ bot.connected ? '停止' : '启动' }}
|
{{ bot.connected ? '停止' : '启动' }}
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn-sm btn-edit" @click="openEditBot(bot)">
|
||||||
|
<Icon icon="mdi:pencil" />
|
||||||
|
</button>
|
||||||
|
<button class="btn-sm btn-delete" @click="deleteBot(bot.id, bot.name)">
|
||||||
|
<Icon icon="mdi:delete" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Bot Modal -->
|
||||||
|
<div v-if="editingBot" class="edit-modal-overlay" @click.self="editingBot = null">
|
||||||
|
<div class="edit-modal">
|
||||||
|
<h3 class="modal-title">编辑机器人</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>名称</label>
|
||||||
|
<input v-model="editForm.name" class="input" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>服务器地址</label>
|
||||||
|
<input v-model="editForm.serverAddress" class="input" placeholder="ts.example.com" />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group" style="flex:1">
|
||||||
|
<label>端口</label>
|
||||||
|
<input v-model.number="editForm.serverPort" type="number" class="input" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="flex:2">
|
||||||
|
<label>昵称</label>
|
||||||
|
<input v-model="editForm.nickname" class="input" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>默认频道(可选)</label>
|
||||||
|
<input v-model="editForm.defaultChannel" class="input" placeholder="音乐频道" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>频道密码(可选)</label>
|
||||||
|
<input v-model="editForm.channelPassword" class="input" type="password" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn-secondary" @click="editingBot = null">取消</button>
|
||||||
|
<button class="btn-primary" @click="saveEditBot">保存(需重启机器人生效)</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Create Bot -->
|
||||||
<div class="create-bot">
|
<div class="create-bot">
|
||||||
<h3 class="subsection-title">创建新实例</h3>
|
<h3 class="subsection-title">创建新实例</h3>
|
||||||
<div class="form-row">
|
<div class="form-group">
|
||||||
<input v-model="newBotName" class="input" placeholder="机器人名称" />
|
<label>名称</label>
|
||||||
<input v-model="newBotServer" class="input" placeholder="TS服务器地址" />
|
<input v-model="newBotName" class="input" placeholder="我的音乐机器人" />
|
||||||
<button class="btn-primary" @click="createBot">创建</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group" style="flex:2">
|
||||||
|
<label>服务器地址</label>
|
||||||
|
<input v-model="newBotServer" class="input" placeholder="localhost 或 ts.example.com" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="flex:1">
|
||||||
|
<label>端口</label>
|
||||||
|
<input v-model.number="newBotPort" type="number" class="input" placeholder="9987" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>昵称</label>
|
||||||
|
<input v-model="newBotNickname" class="input" placeholder="MusicBot" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>默认频道(可选)</label>
|
||||||
|
<input v-model="newBotChannel" class="input" placeholder="音乐频道" />
|
||||||
|
</div>
|
||||||
|
<button class="btn-primary" @click="createBot">创建</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -315,6 +377,21 @@ const store = usePlayerStore();
|
|||||||
|
|
||||||
const newBotName = ref('');
|
const newBotName = ref('');
|
||||||
const newBotServer = ref('');
|
const newBotServer = ref('');
|
||||||
|
const newBotPort = ref(9987);
|
||||||
|
const newBotNickname = ref('MusicBot');
|
||||||
|
const newBotChannel = ref('');
|
||||||
|
|
||||||
|
// Edit bot
|
||||||
|
const editingBot = ref<string | null>(null);
|
||||||
|
const editForm = reactive({
|
||||||
|
name: '',
|
||||||
|
serverAddress: '',
|
||||||
|
serverPort: 9987,
|
||||||
|
nickname: '',
|
||||||
|
defaultChannel: '',
|
||||||
|
channelPassword: '',
|
||||||
|
});
|
||||||
|
|
||||||
const neteaseCookie = ref('');
|
const neteaseCookie = ref('');
|
||||||
const qqCookie = ref('');
|
const qqCookie = ref('');
|
||||||
const bilibiliCookie = ref('');
|
const bilibiliCookie = ref('');
|
||||||
@@ -465,12 +542,59 @@ async function createBot() {
|
|||||||
await axios.post('/api/bot', {
|
await axios.post('/api/bot', {
|
||||||
name: newBotName.value,
|
name: newBotName.value,
|
||||||
serverAddress: newBotServer.value,
|
serverAddress: newBotServer.value,
|
||||||
serverPort: 9987,
|
serverPort: newBotPort.value || 9987,
|
||||||
nickname: newBotName.value,
|
nickname: newBotNickname.value || newBotName.value,
|
||||||
|
defaultChannel: newBotChannel.value || undefined,
|
||||||
autoStart: false,
|
autoStart: false,
|
||||||
});
|
});
|
||||||
newBotName.value = '';
|
newBotName.value = '';
|
||||||
newBotServer.value = '';
|
newBotServer.value = '';
|
||||||
|
newBotPort.value = 9987;
|
||||||
|
newBotNickname.value = 'MusicBot';
|
||||||
|
newBotChannel.value = '';
|
||||||
|
await store.fetchBots();
|
||||||
|
} catch {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteBot(botId: string, botName: string) {
|
||||||
|
if (!confirm(`确认删除机器人 "${botName}"?此操作不可撤销。`)) return;
|
||||||
|
try {
|
||||||
|
await axios.delete(`/api/bot/${botId}`);
|
||||||
|
await store.fetchBots();
|
||||||
|
} catch {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditBot(bot: any) {
|
||||||
|
editingBot.value = bot.id;
|
||||||
|
// We need to fetch the saved config from server
|
||||||
|
// For now use the status info we have
|
||||||
|
editForm.name = bot.name;
|
||||||
|
editForm.serverAddress = '';
|
||||||
|
editForm.serverPort = 9987;
|
||||||
|
editForm.nickname = '';
|
||||||
|
editForm.defaultChannel = '';
|
||||||
|
editForm.channelPassword = '';
|
||||||
|
// Try to get saved config
|
||||||
|
axios.get(`/api/bot/${bot.id}/config`).then(res => {
|
||||||
|
if (res.data) {
|
||||||
|
editForm.serverAddress = res.data.serverAddress ?? '';
|
||||||
|
editForm.serverPort = res.data.serverPort ?? 9987;
|
||||||
|
editForm.nickname = res.data.nickname ?? '';
|
||||||
|
editForm.defaultChannel = res.data.defaultChannel ?? '';
|
||||||
|
editForm.channelPassword = res.data.channelPassword ?? '';
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveEditBot() {
|
||||||
|
if (!editingBot.value) return;
|
||||||
|
try {
|
||||||
|
await axios.put(`/api/bot/${editingBot.value}`, editForm);
|
||||||
|
editingBot.value = null;
|
||||||
await store.fetchBots();
|
await store.fetchBots();
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore
|
// Ignore
|
||||||
@@ -848,6 +972,75 @@ onUnmounted(() => {
|
|||||||
&:hover { background: var(--color-primary); color: white; }
|
&:hover { background: var(--color-primary); color: white; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-edit {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
&:hover { background: #f44336; color: white; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: var(--hover-bg);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 200;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-modal {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 28px;
|
||||||
|
width: 480px;
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.spin {
|
.spin {
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user