feat: 功能2 - 绑定/解绑 Agent (v0.9.7)
- 添加 BotSettingsModal.vue 机器人设置模态框 - 显示在线 Agent 列表 - 实现 Agent 绑定功能 - 实现 Agent 解绑功能 - HomeView 添加设置按钮(hover 显示) - Store 添加 bindBotAgent 方法 - 显示机器人绑定状态
This commit is contained in:
410
frontend/src/components/chat/BotSettingsModal.vue
Normal file
410
frontend/src/components/chat/BotSettingsModal.vue
Normal 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>
|
||||||
@@ -2,14 +2,17 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
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 CreateBotModal from '@/components/chat/CreateBotModal.vue'
|
||||||
|
import BotSettingsModal from '@/components/chat/BotSettingsModal.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const chatStore = useChatStore()
|
const chatStore = useChatStore()
|
||||||
|
|
||||||
const showCreateBot = ref(false)
|
const showCreateBot = ref(false)
|
||||||
|
const showBotSettings = ref(false)
|
||||||
|
const selectedBot = ref<Bot | null>(null)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await chatStore.fetchBots()
|
await chatStore.fetchBots()
|
||||||
@@ -28,9 +31,23 @@ function resumeChat(sessionId: string) {
|
|||||||
router.push({ name: 'chat', params: { sessionId } })
|
router.push({ name: 'chat', params: { sessionId } })
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBotCreated(bot: any) {
|
function handleBotCreated(bot: Bot) {
|
||||||
chatStore.bots.push(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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -53,7 +70,10 @@ function handleBotCreated(bot: any) {
|
|||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<!-- Bot List -->
|
<!-- Bot List -->
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h2 class="section-title">选择机器人开始聊天</h2>
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">选择机器人开始聊天</h2>
|
||||||
|
<span class="bot-count">{{ chatStore.bots.length }} 个机器人</span>
|
||||||
|
</div>
|
||||||
<div class="bot-grid">
|
<div class="bot-grid">
|
||||||
<div
|
<div
|
||||||
v-for="bot in chatStore.bots"
|
v-for="bot in chatStore.bots"
|
||||||
@@ -70,6 +90,12 @@ function handleBotCreated(bot: any) {
|
|||||||
{{ bot.status === 'online' ? '🟢 在线' : '⚪ 离线' }}
|
{{ bot.status === 'online' ? '🟢 在线' : '⚪ 离线' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
class="settings-btn"
|
||||||
|
@click="openBotSettings(bot, $event)"
|
||||||
|
>
|
||||||
|
⚙️
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -110,6 +136,13 @@ function handleBotCreated(bot: any) {
|
|||||||
@close="showCreateBot = false"
|
@close="showCreateBot = false"
|
||||||
@created="handleBotCreated"
|
@created="handleBotCreated"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<BotSettingsModal
|
||||||
|
v-if="showBotSettings && selectedBot"
|
||||||
|
:bot="selectedBot"
|
||||||
|
@close="showBotSettings = false"
|
||||||
|
@updated="handleBotUpdated"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -192,11 +225,22 @@ function handleBotCreated(bot: any) {
|
|||||||
margin-bottom: var(--spacing-xl);
|
margin-bottom: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: var(--font-size-md);
|
font-size: var(--font-size-md);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
margin-bottom: var(--spacing-md);
|
}
|
||||||
|
|
||||||
|
.bot-count {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bot-grid {
|
.bot-grid {
|
||||||
@@ -214,6 +258,7 @@ function handleBotCreated(bot: any) {
|
|||||||
gap: var(--spacing-md);
|
gap: var(--spacing-md);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bot-card:hover {
|
.bot-card:hover {
|
||||||
@@ -221,6 +266,32 @@ function handleBotCreated(bot: any) {
|
|||||||
box-shadow: var(--shadow-md);
|
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 {
|
.bot-avatar {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
|
|||||||
Reference in New Issue
Block a user