feat: 功能2 - 绑定/解绑 Agent (v0.9.7)

- 添加 BotSettingsModal.vue 机器人设置模态框
- 显示在线 Agent 列表
- 实现 Agent 绑定功能
- 实现 Agent 解绑功能
- HomeView 添加设置按钮(hover 显示)
- Store 添加 bindBotAgent 方法
- 显示机器人绑定状态
This commit is contained in:
2026-03-15 11:59:04 +08:00
parent 15b001bab5
commit 021ce8b50b
2 changed files with 485 additions and 4 deletions

View File

@@ -0,0 +1,410 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import type { Bot, Agent } from '@/stores/chat'
const props = defineProps<{
bot: Bot
}>()
const emit = defineEmits<{
close: []
updated: [bot: Bot]
}>()
const agents = ref<Agent[]>([])
const selectedAgentId = ref<string | null>(null)
const loading = ref(false)
const error = ref('')
onMounted(async () => {
await fetchAgents()
selectedAgentId.value = props.bot.agent_id || null
})
async function fetchAgents() {
try {
const response = await fetch('/api/agents/available', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
}
})
const data = await response.json()
agents.value = data.agents || []
} catch (e) {
console.error('Fetch agents failed:', e)
}
}
async function handleBind() {
if (!selectedAgentId.value) {
error.value = '请选择一个 Agent'
return
}
loading.value = true
error.value = ''
try {
const response = await fetch(`/api/bots/${props.bot.id}/bind`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
},
body: JSON.stringify({ agent_id: selectedAgentId.value })
})
if (!response.ok) {
const data = await response.json()
throw new Error(data.error || '绑定失败')
}
const data = await response.json()
emit('updated', data.bot)
emit('close')
} catch (e: any) {
error.value = e.message || '绑定失败'
} finally {
loading.value = false
}
}
async function handleUnbind() {
loading.value = true
error.value = ''
try {
const response = await fetch(`/api/bots/${props.bot.id}/unbind`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
}
})
if (!response.ok) {
const data = await response.json()
throw new Error(data.error || '解绑失败')
}
const data = await response.json()
selectedAgentId.value = null
emit('updated', data.bot)
} catch (e: any) {
error.value = e.message || '解绑失败'
} finally {
loading.value = false
}
}
</script>
<template>
<div class="modal-overlay" @click.self="emit('close')">
<div class="modal">
<div class="modal-header">
<h3>机器人设置</h3>
<button class="close-btn" @click="emit('close')">×</button>
</div>
<div class="modal-body">
<!-- Bot Info -->
<div class="bot-info">
<div class="bot-avatar">{{ bot.avatar || '🤖' }}</div>
<div class="bot-details">
<h4>{{ bot.display_name || bot.name }}</h4>
<p class="bot-status" :class="bot.status">
{{ bot.status === 'online' ? '🟢 在线' : '⚪ 离线' }}
</p>
</div>
</div>
<!-- Agent Selection -->
<div class="form-group">
<label>绑定 Agent</label>
<p class="hint">选择一个在线的 Agent 来启用此机器人</p>
<div class="agent-list" v-if="agents.length > 0">
<label
v-for="agent in agents"
:key="agent.id"
class="agent-item"
:class="{ selected: selectedAgentId === agent.id }"
>
<input
type="radio"
:value="agent.id"
v-model="selectedAgentId"
/>
<div class="agent-avatar">🤖</div>
<div class="agent-info">
<span class="agent-name">{{ agent.display_name || agent.name }}</span>
<span class="agent-status" :class="agent.status">
{{ agent.status === 'online' ? '在线' : '离线' }}
</span>
</div>
</label>
</div>
<div class="empty" v-else>
<p>暂无可用 Agent</p>
<p class="hint">请确保至少有一个 Agent 在线</p>
</div>
</div>
<!-- Error -->
<div v-if="error" class="error-message">
{{ error }}
</div>
<!-- Actions -->
<div class="modal-actions">
<button
v-if="bot.agent_id"
type="button"
class="btn-unbind"
@click="handleUnbind"
:disabled="loading"
>
解绑
</button>
<button
type="button"
class="btn-bind"
@click="handleBind"
:disabled="loading || !selectedAgentId"
>
{{ loading ? '处理中...' : '绑定' }}
</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal {
background: var(--bg-primary);
border-radius: var(--border-radius-lg);
width: 100%;
max-width: 480px;
overflow: hidden;
}
.modal-header {
padding: var(--spacing-md) var(--spacing-lg);
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
font-size: var(--font-size-lg);
font-weight: 600;
}
.close-btn {
width: 32px;
height: 32px;
border: none;
background: transparent;
font-size: 24px;
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.close-btn:hover {
background: var(--bg-secondary);
}
.modal-body {
padding: var(--spacing-lg);
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.bot-info {
display: flex;
align-items: center;
gap: var(--spacing-md);
padding: var(--spacing-md);
background: var(--bg-secondary);
border-radius: var(--border-radius);
}
.bot-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--bg-tertiary);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.bot-details h4 {
font-size: var(--font-size-md);
font-weight: 600;
margin-bottom: var(--spacing-xs);
}
.bot-status {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.bot-status.online {
color: var(--success-color);
}
.form-group {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.form-group label {
font-size: var(--font-size-sm);
font-weight: 500;
}
.hint {
font-size: var(--font-size-xs);
color: var(--text-tertiary);
}
.agent-list {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
max-height: 240px;
overflow-y: auto;
}
.agent-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
cursor: pointer;
transition: all 0.2s;
}
.agent-item:hover {
background: var(--bg-secondary);
}
.agent-item.selected {
border-color: var(--primary-color);
background: rgba(99, 102, 241, 0.1);
}
.agent-item input {
display: none;
}
.agent-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--bg-tertiary);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.agent-info {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.agent-name {
font-size: var(--font-size-sm);
font-weight: 500;
}
.agent-status {
font-size: var(--font-size-xs);
color: var(--text-tertiary);
}
.agent-status.online {
color: var(--success-color);
}
.empty {
text-align: center;
padding: var(--spacing-lg);
color: var(--text-tertiary);
}
.error-message {
color: var(--error-color);
font-size: var(--font-size-sm);
padding: var(--spacing-sm);
background: rgba(239, 68, 68, 0.1);
border-radius: var(--border-radius);
}
.modal-actions {
display: flex;
gap: var(--spacing-sm);
}
.btn-unbind,
.btn-bind {
flex: 1;
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--border-radius);
font-size: var(--font-size-md);
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-unbind {
background: transparent;
border: 1px solid var(--error-color);
color: var(--error-color);
}
.btn-unbind:hover:not(:disabled) {
background: rgba(239, 68, 68, 0.1);
}
.btn-bind {
background: var(--primary-color);
border: none;
color: white;
}
.btn-bind:hover:not(:disabled) {
background: var(--primary-dark);
}
.btn-unbind:disabled,
.btn-bind:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>

View File

@@ -2,14 +2,17 @@
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useChatStore } from '@/stores/chat'
import { useChatStore, type Bot } from '@/stores/chat'
import CreateBotModal from '@/components/chat/CreateBotModal.vue'
import BotSettingsModal from '@/components/chat/BotSettingsModal.vue'
const router = useRouter()
const authStore = useAuthStore()
const chatStore = useChatStore()
const showCreateBot = ref(false)
const showBotSettings = ref(false)
const selectedBot = ref<Bot | null>(null)
onMounted(async () => {
await chatStore.fetchBots()
@@ -28,9 +31,23 @@ function resumeChat(sessionId: string) {
router.push({ name: 'chat', params: { sessionId } })
}
function handleBotCreated(bot: any) {
function handleBotCreated(bot: Bot) {
chatStore.bots.push(bot)
}
function openBotSettings(bot: Bot, event: Event) {
event.stopPropagation()
selectedBot.value = bot
showBotSettings.value = true
}
function handleBotUpdated(bot: Bot) {
const index = chatStore.bots.findIndex(b => b.id === bot.id)
if (index !== -1) {
chatStore.bots[index] = bot
}
selectedBot.value = bot
}
</script>
<template>
@@ -53,7 +70,10 @@ function handleBotCreated(bot: any) {
<main class="main-content">
<!-- Bot List -->
<section class="section">
<div class="section-header">
<h2 class="section-title">选择机器人开始聊天</h2>
<span class="bot-count">{{ chatStore.bots.length }} 个机器人</span>
</div>
<div class="bot-grid">
<div
v-for="bot in chatStore.bots"
@@ -70,6 +90,12 @@ function handleBotCreated(bot: any) {
{{ bot.status === 'online' ? '🟢 在线' : '⚪ 离线' }}
</p>
</div>
<button
class="settings-btn"
@click="openBotSettings(bot, $event)"
>
</button>
</div>
</div>
</section>
@@ -110,6 +136,13 @@ function handleBotCreated(bot: any) {
@close="showCreateBot = false"
@created="handleBotCreated"
/>
<BotSettingsModal
v-if="showBotSettings && selectedBot"
:bot="selectedBot"
@close="showBotSettings = false"
@updated="handleBotUpdated"
/>
</div>
</template>
@@ -192,11 +225,22 @@ function handleBotCreated(bot: any) {
margin-bottom: var(--spacing-xl);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
}
.section-title {
font-size: var(--font-size-md);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-md);
}
.bot-count {
font-size: var(--font-size-sm);
color: var(--text-tertiary);
}
.bot-grid {
@@ -214,6 +258,7 @@ function handleBotCreated(bot: any) {
gap: var(--spacing-md);
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
position: relative;
}
.bot-card:hover {
@@ -221,6 +266,32 @@ function handleBotCreated(bot: any) {
box-shadow: var(--shadow-md);
}
.settings-btn {
position: absolute;
top: 8px;
right: 8px;
width: 28px;
height: 28px;
border: none;
background: var(--bg-tertiary);
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
opacity: 0;
transition: opacity 0.2s;
}
.bot-card:hover .settings-btn {
opacity: 1;
}
.settings-btn:hover {
background: var(--bg-primary);
}
.bot-avatar {
width: 48px;
height: 48px;