From 4045dad903027bd200d611a9e7bca7f39311ed06 Mon Sep 17 00:00:00 2001 From: "DESKTOP-72TV0V4\\caoxiaozhu" Date: Fri, 13 Mar 2026 08:32:08 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20Agents=E9=A1=B5=E9=9D=A2=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E4=B8=BA=E7=8B=AC=E7=AB=8B=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 web/src/views/agents/useAgents.ts: Agents页面的组合式函数 - 新增 web/src/views/agents/agents.css: Agents页面样式文件 - 精简 web/src/views/Agents.vue: 保留主入口,引用新的模块 将Agents页面的逻辑拆分为独立的TS文件和CSS文件,提升代码可维护性 Co-Authored-By: Claude Opus 4.6 --- web/src/views/Agents.vue | 1020 ++--------------------------- web/src/views/agents/agents.css | 370 +++++++++++ web/src/views/agents/useAgents.ts | 591 +++++++++++++++++ 3 files changed, 1015 insertions(+), 966 deletions(-) create mode 100644 web/src/views/agents/agents.css create mode 100644 web/src/views/agents/useAgents.ts diff --git a/web/src/views/Agents.vue b/web/src/views/Agents.vue index 3bb4dbb..fd7bfd9 100644 --- a/web/src/views/Agents.vue +++ b/web/src/views/Agents.vue @@ -1,549 +1,60 @@ - - diff --git a/web/src/views/agents/agents.css b/web/src/views/agents/agents.css new file mode 100644 index 0000000..04e83ca --- /dev/null +++ b/web/src/views/agents/agents.css @@ -0,0 +1,370 @@ +/* Skills Selector Styles */ +.skills-selector { + position: relative; +} + +/* Skills Mode Options */ +.skills-mode-options { + padding: 8px; + border-bottom: 1px solid #2a2c36; +} + +.skills-mode-item { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 12px; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; +} + +.skills-mode-item:hover { + background: #1a1c25; +} + +.skills-mode-item.active { + background: rgba(249, 115, 22, 0.1); +} + +.mode-radio { + width: 18px; + height: 18px; + border: 2px solid #6b7280; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-top: 2px; +} + +.skills-mode-item.active .mode-radio { + border-color: #f97316; +} + +.radio-dot { + width: 10px; + height: 10px; + background: #f97316; + border-radius: 50%; +} + +.mode-content { + display: flex; + flex-direction: column; + gap: 2px; +} + +.mode-label { + font-size: 14px; + font-weight: 500; + color: #f3f4f6; +} + +.mode-desc { + font-size: 12px; + color: #9ca3af; +} + +/* 子下拉框箭头 */ +.sub-arrow { + margin-left: auto; + color: #9ca3af; + transition: transform 0.2s ease; +} + +.skills-mode-item:hover .sub-arrow { + color: #f97316; +} + +/* 子下拉面板 */ +.sub-dropdown-panel { + position: absolute; + top: 0; + left: 100%; + margin-left: 8px; + width: 280px; + background: #171922; + border: 1px solid #2a2c36; + border-radius: 12px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4); + z-index: 100; + overflow: hidden; +} + +.sub-dropdown-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid #2a2c36; + background: #1a1c25; +} + +.sub-dropdown-title { + font-size: 13px; + font-weight: 500; + color: #f3f4f6; +} + +.sub-dropdown-close { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + color: #9ca3af; + transition: all 0.2s ease; +} + +.sub-dropdown-close:hover { + background: #374151; + color: #f3f4f6; +} + +/* 无结果样式 */ +.no-results { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 24px; + color: #6b7280; + gap: 8px; +} + +.no-results-icon { + opacity: 0.5; +} + +.selected-tags { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 44px; + padding: 0 12px; + background: #171922; + border: 1px solid #2a2c36; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; +} + +.selected-tags:hover { + border-color: #f97316; +} + +.selected-tags .placeholder { + color: #9ca3af; + font-size: 14px; +} + +.tags-container { + display: flex; + flex-wrap: wrap; + gap: 6px; + flex: 1; + overflow: hidden; +} + +.selected-tag { + display: inline-flex; + align-items: center; + padding: 2px 8px; + background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); + color: white; + font-size: 12px; + font-weight: 500; + border-radius: 4px; + white-space: nowrap; +} + +.more-tag { + display: inline-flex; + align-items: center; + padding: 2px 8px; + background: #374151; + color: #d1d5db; + font-size: 12px; + border-radius: 4px; +} + +.dropdown-icon { + color: #9ca3af; + transition: transform 0.2s ease; + flex-shrink: 0; +} + +.dropdown-icon.rotate-180 { + transform: rotate(180deg); +} + +/* Dropdown Panel */ +.dropdown-panel { + position: absolute; + top: calc(100% + 8px); + left: 0; + right: 0; + background: #171922; + border: 1px solid #2a2c36; + border-radius: 12px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4); + z-index: 100; + overflow: hidden; +} + +/* Search Box */ +.search-box { + position: relative; + padding: 12px; + border-bottom: 1px solid #2a2c36; +} + +.search-icon { + position: absolute; + left: 24px; + top: 50%; + transform: translateY(-50%); + color: #9ca3af; +} + +.search-input { + width: 100%; + padding: 8px 12px 8px 36px; + background: #111827; + border: 1px solid #374151; + border-radius: 6px; + color: white; + font-size: 14px; + outline: none; + transition: border-color 0.2s ease; +} + +.search-input:focus { + border-color: #f97316; +} + +.search-input::placeholder { + color: #6b7280; +} + +/* Action Bar */ +.action-bar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 12px; + background: #111827; + border-bottom: 1px solid #374151; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +.checkbox { + width: 16px; + height: 16px; + accent-color: #f97316; + cursor: pointer; +} + +.checkbox-text { + font-size: 13px; + font-weight: 500; + color: #f97316; +} + +.clear-btn { + padding: 4px 10px; + background: transparent; + border: 1px solid #4b5563; + border-radius: 4px; + color: #9ca3af; + font-size: 12px; + cursor: pointer; + transition: all 0.2s ease; +} + +.clear-btn:hover { + background: #374151; + color: white; +} + +/* Options List */ +.options-list { + max-height: 240px; + overflow-y: auto; + padding: 8px; +} + +.options-list::-webkit-scrollbar { + width: 6px; +} + +.options-list::-webkit-scrollbar-track { + background: #171922; +} + +.options-list::-webkit-scrollbar-thumb { + background: #4b5563; + border-radius: 3px; +} + +.option-item { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 10px 12px; + border-radius: 8px; + cursor: pointer; + transition: background 0.15s ease; +} + +.option-item:hover { + background: #374151; +} + +.option-item .checkbox { + margin-top: 2px; + flex-shrink: 0; +} + +.option-content { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + flex: 1; +} + +.option-label { + font-size: 14px; + font-weight: 500; + color: white; +} + +.option-desc { + font-size: 12px; + color: #9ca3af; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Dropdown Transition */ +.dropdown-enter-active, +.dropdown-leave-active { + transition: all 0.2s ease; +} + +.dropdown-enter-from, +.dropdown-leave-to { + opacity: 0; + transform: translateY(-8px); +} diff --git a/web/src/views/agents/useAgents.ts b/web/src/views/agents/useAgents.ts new file mode 100644 index 0000000..e9b95f4 --- /dev/null +++ b/web/src/views/agents/useAgents.ts @@ -0,0 +1,591 @@ +// Agent API 调用和状态管理 + +import { ref, computed } from 'vue' +import { ElMessage } from 'element-plus' +import { formatDate } from '@/utils/format' + +const API_BASE = 'http://localhost:8082' + +// 类型定义 +export interface Skill { + id: string + skill_name: string + skill_type: string + skill_desc: string + path: string + status: string +} + +export interface ModelOption { + id: string + name: string + provider: string + model: string +} + +export interface Agent { + id: string + name: string + avatar: string + description: string + accentColor: string + gradient: string + status: string + skills: string + model: string + createdAt: string +} + +export interface AgentFormData { + name: string + description: string + avatar: string + skillsMode: 'all' | 'include' | 'exclude' + selectedSkills: string[] + knowledge: string + prompt: string + modelId: string +} + +// 状态 +const agents = ref([]) +const skillsList = ref([]) +const modelsList = ref([]) +const searchQuery = ref('') +const filterStatus = ref('all') +const isLoading = ref(false) +const skillsLoading = ref(false) +const modelsLoading = ref(false) + +// 创建弹窗 +const showCreateModal = ref(false) +const isCreating = ref(false) +const newAgent = ref({ + name: '', + description: '', + avatar: '🤖', + skillsMode: 'all', + selectedSkills: [], + knowledge: '', + prompt: '', + modelId: '' +}) + +// 编辑弹窗 +const showEditModal = ref(false) +const isEditing = ref(false) +const editingAgent = ref({ + id: '', + name: '', + description: '', + avatar: '🤖', + skillsMode: 'all', + selectedSkills: [], + knowledge: '', + prompt: '', + modelId: '' +}) + +// Skills 选择器 +const showSkillsDropdown = ref(false) +const showSubSkillsDropdown = ref(false) +const skillsSearch = ref('') + +// 选项 +const skillsModeOptions = [ + { value: 'all', label: 'All Skills', desc: 'Use all available skills' }, + { value: 'include', label: 'Include Skills', desc: 'Select specific skills to use' }, + { value: 'exclude', label: 'Exclude Skills', desc: 'Select skills to exclude' }, +] + +const avatarOptions = [ + '🤖', '🧠', '💻', '📊', '🔬', '🎧', '✨', '💬', '🔮', '🌙', + '🐉', '☁️', '🎨', '🎯', '🚀', '⚡', '🔥', '💡', '🎭', '🎪' +] + +const knowledgeOptions = [ + { value: 'general', label: 'General Knowledge' }, + { value: 'codebase', label: 'Codebase' }, + { value: 'docs', label: 'Documentation' }, + { value: 'api', label: 'API Reference' }, +] + +// 计算属性 +const skillsOptions = computed(() => { + return skillsList.value.map(skill => ({ + value: skill.id, + label: skill.skill_name, + desc: skill.skill_desc + })) +}) + +const filteredSkills = computed(() => { + if (!skillsSearch.value) return skillsOptions.value + const search = skillsSearch.value.toLowerCase() + return skillsOptions.value.filter(s => + s.label.toLowerCase().includes(search) || + (s.desc && s.desc.toLowerCase().includes(search)) + ) +}) + +const filteredAgents = computed(() => { + return agents.value.filter(agent => { + const matchSearch = agent.name.toLowerCase().includes(searchQuery.value.toLowerCase()) || + agent.skills.toLowerCase().includes(searchQuery.value.toLowerCase()) + const matchStatus = filterStatus.value === 'all' || agent.status === filterStatus.value + return matchSearch && matchStatus + }) +}) + +const stats = computed(() => ({ + total: agents.value.length, + active: agents.value.filter(a => a.status === 'active').length, + inactive: agents.value.filter(a => a.status === 'inactive').length, +})) + +const isAllSelected = computed(() => { + return skillsOptions.value.length > 0 && newAgent.value.selectedSkills.length === skillsOptions.value.length +}) + +const isIndeterminate = computed(() => { + return newAgent.value.selectedSkills.length > 0 && newAgent.value.selectedSkills.length < skillsOptions.value.length +}) + +// 方法 +async function fetchAgents() { + try { + const response = await fetch(`${API_BASE}/api/agent/list`) + if (!response.ok) throw new Error('Failed to fetch agents') + const data = await response.json() + agents.value = (data.agents || []).map((agent: any) => ({ + id: agent.id, + name: agent.name, + avatar: agent.avatar || '🤖', + description: agent.description || '', + accentColor: '#f97316', + gradient: 'from-orange-500/20 to-amber-500/20', + status: agent.is_active ? 'active' : 'inactive', + skills: agent.skills?.length > 0 ? agent.skills.join(', ') : 'None', + model: agent.model_name || 'None', + createdAt: agent.created_at ? formatDate(agent.created_at, 'YYYY/MM/DD HH:mm') : formatDate(new Date(), 'YYYY/MM/DD HH:mm'), + })) + } catch (error) { + console.error('Failed to fetch agents:', error) + } +} + +async function fetchSkills() { + skillsLoading.value = true + try { + const response = await fetch(`${API_BASE}/skill/list`) + if (!response.ok) { + console.error('Failed to fetch skills:', response.status, response.statusText) + return + } + const result = await response.json() + if (result.list) { + skillsList.value = result.list + } + } catch (error) { + console.error('Failed to fetch skills:', error) + } finally { + skillsLoading.value = false + } +} + +async function fetchModels() { + modelsLoading.value = true + try { + const response = await fetch(`${API_BASE}/model/list`) + if (!response.ok) { + console.error('Failed to fetch models:', response.status, response.statusText) + return + } + const result = await response.json() + if (result.list) { + modelsList.value = result.list.map((m: any) => ({ + id: m.id, + name: m.name, + provider: m.provider, + model: m.model + })) + } + } catch (error) { + console.error('Failed to fetch models:', error) + } finally { + modelsLoading.value = false + } +} + +function getSkillLabel(id: string) { + return skillsOptions.value.find(s => s.value === id)?.label || id +} + +function openCreateModal() { + newAgent.value = { + name: '', + description: '', + avatar: '🤖', + skillsMode: 'all', + selectedSkills: [], + knowledge: '', + prompt: '', + modelId: '' + } + showCreateModal.value = true +} + +async function createAgent() { + if (!newAgent.value.name || (newAgent.value.skillsMode !== 'all' && newAgent.value.selectedSkills.length === 0)) { + return + } + + const selectedModel = modelsList.value.find(m => m.id === newAgent.value.modelId) + isCreating.value = true + + try { + const skills = newAgent.value.skillsMode === 'all' ? ['*'] : newAgent.value.selectedSkills + + const response = await fetch(`${API_BASE}/api/agent/create`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: newAgent.value.name, + description: newAgent.value.description, + avatar: newAgent.value.avatar, + skillsMode: newAgent.value.skillsMode, + skills: skills, + knowledge: newAgent.value.knowledge, + prompt: newAgent.value.prompt, + model_provider: selectedModel?.provider, + model_name: selectedModel?.name + }) + }) + + if (!response.ok) throw new Error('Failed to create agent') + + const result = await response.json() + const skillsLabels = newAgent.value.selectedSkills.map(id => getSkillLabel(id)).join(', ') + + agents.value.unshift({ + id: result.agent_id, + name: newAgent.value.name, + avatar: newAgent.value.avatar, + description: newAgent.value.description, + accentColor: '#f97316', + gradient: 'from-orange-500/20 to-amber-500/20', + status: 'inactive', + skills: skillsLabels || 'None', + model: selectedModel?.name || 'None', + createdAt: formatDate(new Date(), 'YYYY/MM/DD HH:mm') + }) + + showCreateModal.value = false + ElMessage.success('Agent created successfully') + } catch (error) { + console.error('Failed to create agent:', error) + ElMessage.error('Failed to create agent') + } finally { + isCreating.value = false + } +} + +function openEdit(agent: Agent) { + let selectedSkills: string[] = [] + let skillsMode: 'all' | 'include' | 'exclude' = 'all' + + if (agent.skills === '*') { + skillsMode = 'all' + } else if (agent.skills && agent.skills !== 'None') { + skillsMode = 'include' + selectedSkills = agent.skills.split(',').map((s: string) => s.trim()) + } + + const model = modelsList.value.find(m => m.name === agent.model) + + editingAgent.value = { + id: agent.id, + name: agent.name, + description: agent.description || '', + avatar: agent.avatar || '🤖', + skillsMode, + selectedSkills, + modelId: model?.id || '', + prompt: '', + knowledge: '' + } + showEditModal.value = true +} + +async function saveEdit() { + if (!editingAgent.value.name) return + + const skills = editingAgent.value.skillsMode === 'all' ? ['*'] : editingAgent.value.selectedSkills + const selectedModel = modelsList.value.find(m => m.id === editingAgent.value.modelId) + isEditing.value = true + + try { + const response = await fetch(`${API_BASE}/api/agent/${editingAgent.value.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: editingAgent.value.name, + description: editingAgent.value.description, + skills: skills, + role_description: editingAgent.value.prompt, + model_provider: selectedModel?.provider || '', + model_name: selectedModel?.name || '' + }) + }) + + if (response.ok) { + const agent = agents.value.find(a => a.id === editingAgent.value.id) + if (agent) { + agent.name = editingAgent.value.name + agent.description = editingAgent.value.description + agent.skills = editingAgent.value.skillsMode === 'all' ? '*' : editingAgent.value.selectedSkills.join(', ') + agent.model = selectedModel?.name || '' + } + showEditModal.value = false + ElMessage.success('Agent updated successfully') + } else { + ElMessage.error('Failed to update agent') + } + } catch (error) { + console.error('Failed to update agent:', error) + ElMessage.error('Failed to update agent') + } finally { + isEditing.value = false + } +} + +async function toggleStatus(agent: Agent) { + const newStatus = agent.status === 'active' ? false : true + try { + const response = await fetch(`${API_BASE}/api/agent/${agent.id}/status`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ is_active: newStatus }) + }) + if (response.ok) { + agent.status = newStatus ? 'active' : 'inactive' + } else { + ElMessage.error('Failed to update status') + } + } catch (error) { + console.error('Failed to update status:', error) + ElMessage.error('Failed to update status') + } +} + +async function deleteAgent(id: string) { + try { + const response = await fetch(`${API_BASE}/api/agent/${id}`, { method: 'DELETE' }) + if (response.ok) { + agents.value = agents.value.filter(a => a.id !== id) + ElMessage.success('Agent deleted successfully') + } else { + ElMessage.error('Failed to delete agent') + } + } catch (error) { + console.error('Failed to delete agent:', error) + ElMessage.error('Failed to delete agent') + } +} + +function toggleSkillsDropdown() { + showSkillsDropdown.value = !showSkillsDropdown.value + if (!showSkillsDropdown.value) { + skillsSearch.value = '' + } +} + +function closeSkillsDropdown() { + showSkillsDropdown.value = false + skillsSearch.value = '' +} + +function handleSkillsModeClick(mode: string) { + if (mode === 'all' || mode === 'include' || mode === 'exclude') { + newAgent.value.skillsMode = mode + if (mode === 'all') { + newAgent.value.selectedSkills = [] + } + showSkillsDropdown.value = false + if (mode === 'include' || mode === 'exclude') { + showSubSkillsDropdown.value = true + } + } +} + +function handleSkillsModeClickEdit(mode: string) { + if (mode === 'all' || mode === 'include' || mode === 'exclude') { + editingAgent.value.skillsMode = mode + if (mode === 'all') { + editingAgent.value.selectedSkills = [] + } + showSkillsDropdown.value = false + if (mode === 'include' || mode === 'exclude') { + showSubSkillsDropdown.value = true + } + } +} + +function toggleSelectAll() { + if (isAllSelected.value) { + newAgent.value.selectedSkills = [] + } else { + newAgent.value.selectedSkills = skillsOptions.value.map(s => s.value) + } +} + +function clearSkills() { + newAgent.value.selectedSkills = [] +} + +// 切换技能模式下拉框 +function toggleSkillsMode() { + showSkillsDropdown.value = !showSkillsDropdown.value + showSubSkillsDropdown.value = false +} + +// 选择技能模式 +function selectSkillsMode(mode: 'all' | 'include' | 'exclude') { + newAgent.value.skillsMode = mode + if (mode === 'all') { + newAgent.value.selectedSkills = [] + } + showSkillsDropdown.value = false + if (mode === 'include' || mode === 'exclude') { + showSubSkillsDropdown.value = true + } +} + +// 切换子下拉框 +function toggleSubSkillsDropdown() { + showSubSkillsDropdown.value = !showSubSkillsDropdown.value +} + +// 关闭所有下拉框 +function closeAllDropdowns() { + showSkillsDropdown.value = false + showSubSkillsDropdown.value = false +} + +// 获取显示文本 +function getSkillsDisplayText() { + if (newAgent.value.skillsMode === 'all') { + return 'All Skills' + } + const count = newAgent.value.selectedSkills.length + if (count === 0) { + return newAgent.value.skillsMode === 'include' ? 'Select skills to include...' : 'Select skills to exclude...' + } + return `${count} skill${count > 1 ? 's' : ''} ${newAgent.value.skillsMode === 'include' ? 'included' : 'excluded'}` +} + +// 切换子下拉框中的技能选择 +function toggleSkillSelection(skillId: string) { + const index = newAgent.value.selectedSkills.indexOf(skillId) + if (index > -1) { + newAgent.value.selectedSkills.splice(index, 1) + } else { + newAgent.value.selectedSkills.push(skillId) + } +} + +// 全选 skills +function selectAllSkills() { + newAgent.value.selectedSkills = skillsOptions.value.map(s => s.value) +} + +// 状态颜色 +function statusClass(status: string) { + switch (status) { + case 'active': return 'bg-green-500' + case 'inactive': return 'bg-gray-500' + default: return 'bg-gray-500' + } +} + +function handleClickOutside(e: MouseEvent) { + const target = e.target as HTMLElement + if (!target.closest('.skills-selector')) { + closeSkillsDropdown() + } +} + +// 初始化 - 在 useAgents 函数中导出初始化函数 +function init() { + fetchSkills() + fetchModels() + fetchAgents() + document.addEventListener('click', handleClickOutside) +} + +function cleanup() { + document.removeEventListener('click', handleClickOutside) +} + +// 导出 +export function useAgents() { + // 在 useAgents 被调用时初始化(组件挂载时) + init() + + return { + // 状态 + agents, + skillsList, + modelsList, + searchQuery, + filterStatus, + isLoading, + skillsLoading, + modelsLoading, + showCreateModal, + isCreating, + newAgent, + showEditModal, + isEditing, + editingAgent, + showSkillsDropdown, + showSubSkillsDropdown, + skillsSearch, + skillsModeOptions, + avatarOptions, + knowledgeOptions, + skillsOptions, + filteredSkills, + filteredAgents, + stats, + isAllSelected, + isIndeterminate, + // 方法 + fetchAgents, + fetchSkills, + fetchModels, + getSkillLabel, + openCreateModal, + createAgent, + openEdit, + saveEdit, + toggleStatus, + deleteAgent, + toggleSkillsDropdown, + closeSkillsDropdown, + handleSkillsModeClick, + handleSkillsModeClickEdit, + toggleSelectAll, + clearSkills, + handleClickOutside, + toggleSkillsMode, + selectSkillsMode, + toggleSubSkillsDropdown, + closeAllDropdowns, + getSkillsDisplayText, + toggleSkillSelection, + selectAllSkills, + statusClass, + cleanup + } +}