Files
aiagent/frontend/src/views/Agents.vue
2026-01-20 11:03:55 +08:00

666 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<MainLayout>
<div class="agents-page">
<el-card>
<template #header>
<div class="card-header">
<h2>Agent管理</h2>
<div>
<el-button @click="handleImport">
<el-icon><Upload /></el-icon>
导入Agent
</el-button>
<el-button type="primary" @click="handleCreate" style="margin-left: 10px">
<el-icon><Plus /></el-icon>
创建Agent
</el-button>
</div>
</div>
</template>
<!-- 搜索和筛选 -->
<div class="filters">
<el-input
v-model="searchText"
placeholder="搜索Agent名称或描述"
style="width: 300px"
clearable
@clear="handleSearch"
@keyup.enter="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-select
v-model="statusFilter"
placeholder="状态筛选"
style="width: 150px; margin-left: 10px"
clearable
@change="handleSearch"
>
<el-option label="草稿" value="draft" />
<el-option label="已发布" value="published" />
<el-option label="运行中" value="running" />
<el-option label="已停止" value="stopped" />
</el-select>
<el-button type="primary" @click="handleSearch" style="margin-left: 10px">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleRefresh">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
<!-- Agent列表 -->
<el-table
v-loading="agentStore.loading"
:data="agentStore.agents"
style="width: 100%; margin-top: 20px"
stripe
>
<el-table-column prop="name" label="名称" width="200" />
<el-table-column prop="description" label="描述" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="version" label="版本" width="80" />
<el-table-column prop="created_at" label="创建时间" width="180">
<template #default="{ row }">
{{ formatDate(row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="480" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleEdit(row)">
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button
v-if="row.status === 'published' || row.status === 'running'"
link
type="primary"
@click="handleUse(row)"
>
<el-icon><ChatDotRound /></el-icon>
使用
</el-button>
<el-button link type="primary" @click="handleDesign(row)">
<el-icon><Setting /></el-icon>
设计
</el-button>
<el-button link type="info" @click="handleDuplicate(row)">
<el-icon><CopyDocument /></el-icon>
复制
</el-button>
<el-button link type="success" @click="handleExport(row)">
<el-icon><Download /></el-icon>
导出
</el-button>
<el-button
v-if="row.status === 'draft' || row.status === 'stopped'"
link
type="success"
@click="handleDeploy(row)"
>
<el-icon><VideoPlay /></el-icon>
部署
</el-button>
<el-button
v-if="row.status === 'published' || row.status === 'running'"
link
type="warning"
@click="handleStop(row)"
>
<el-icon><VideoPause /></el-icon>
停止
</el-button>
<el-button link type="danger" @click="handleDelete(row)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handlePageChange"
@current-change="handlePageChange"
/>
</div>
</el-card>
<!-- 创建/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="600px"
@close="handleDialogClose"
>
<el-form :model="form" label-width="100px" :rules="rules" ref="formRef">
<el-form-item label="名称" prop="name">
<el-input v-model="form.name" placeholder="请输入Agent名称" />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input
v-model="form.description"
type="textarea"
:rows="3"
placeholder="请输入Agent描述"
/>
</el-form-item>
<el-form-item label="工作流配置" prop="workflow_config">
<el-alert
type="info"
:closable="false"
show-icon
>
<template #title>
<div style="font-size: 12px;">
工作流配置将在设计器中编辑创建后可以点击"设计"按钮配置工作流
</div>
</template>
</el-alert>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">
确定
</el-button>
</template>
</el-dialog>
<!-- 导入Agent对话框 -->
<el-dialog
v-model="importDialogVisible"
title="导入Agent"
width="600px"
@close="handleImportDialogClose"
>
<el-upload
ref="uploadRef"
:auto-upload="false"
:on-change="handleFileChange"
:file-list="fileList"
accept=".json"
drag
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将JSON文件拖到此处<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
只能上传JSON格式的Agent配置文件
</div>
</template>
</el-upload>
<div v-if="importFileContent" style="margin-top: 20px;">
<el-alert
type="info"
:closable="false"
show-icon
>
<template #title>
<div>
<div><strong>Agent名称:</strong> {{ importFileContent.name || '未命名' }}</div>
<div v-if="importFileContent.description" style="margin-top: 5px;">
<strong>描述:</strong> {{ importFileContent.description }}
</div>
<div style="margin-top: 5px;">
<strong>节点数:</strong> {{ importFileContent.workflow_config?.nodes?.length || 0 }}
<span style="margin-left: 20px;">
<strong>连接数:</strong> {{ importFileContent.workflow_config?.edges?.length || 0 }}
</span>
</div>
</div>
</template>
</el-alert>
</div>
<template #footer>
<el-button @click="importDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleConfirmImport" :loading="importing" :disabled="!importFileContent">
确认导入
</el-button>
</template>
</el-dialog>
</div>
</MainLayout>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import MainLayout from '@/components/MainLayout.vue'
import {
Plus,
Search,
Refresh,
Edit,
Delete,
Setting,
VideoPlay,
VideoPause,
CopyDocument,
Upload,
Download,
UploadFilled,
ChatDotRound
} from '@element-plus/icons-vue'
import { useAgentStore } from '@/stores/agent'
import type { Agent } from '@/stores/agent'
const router = useRouter()
const agentStore = useAgentStore()
// 搜索和筛选
const searchText = ref('')
const statusFilter = ref('')
const currentPage = ref(1)
const pageSize = ref(20)
const total = ref(0)
// 对话框
const dialogVisible = ref(false)
const dialogTitle = ref('创建Agent')
const formRef = ref()
const submitting = ref(false)
const form = ref({
name: '',
description: '',
workflow_config: {
nodes: [
{
id: 'start-1',
type: 'start',
position: { x: 0, y: 0 },
data: { label: '开始' }
},
{
id: 'end-1',
type: 'end',
position: { x: 200, y: 0 },
data: { label: '结束' }
}
],
edges: []
}
})
const currentAgentId = ref<string | null>(null)
// 导入相关
const importDialogVisible = ref(false)
const fileList = ref<any[]>([])
const importFileContent = ref<any>(null)
const importing = ref(false)
const uploadRef = ref()
// 表单验证规则
const rules = {
name: [
{ required: true, message: '请输入Agent名称', trigger: 'blur' },
{ min: 1, max: 100, message: '名称长度在1到100个字符', trigger: 'blur' }
]
}
// 获取状态类型
const getStatusType = (status: string) => {
const typeMap: Record<string, string> = {
draft: 'info',
published: 'success',
running: 'success',
stopped: 'warning'
}
return typeMap[status] || 'info'
}
// 获取状态文本
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
draft: '草稿',
published: '已发布',
running: '运行中',
stopped: '已停止'
}
return textMap[status] || status
}
// 格式化日期
const formatDate = (dateStr: string) => {
if (!dateStr) return ''
const date = new Date(dateStr)
return date.toLocaleString('zh-CN')
}
// 搜索
const handleSearch = async () => {
currentPage.value = 1
await loadAgents()
}
// 刷新
const handleRefresh = async () => {
searchText.value = ''
statusFilter.value = ''
currentPage.value = 1
await loadAgents()
}
// 加载Agent列表
const loadAgents = async () => {
try {
const agents = await agentStore.fetchAgents({
search: searchText.value || undefined,
status: statusFilter.value || undefined,
skip: (currentPage.value - 1) * pageSize.value,
limit: pageSize.value
})
total.value = agents.length
} catch (error: any) {
ElMessage.error(error.response?.data?.detail || '加载Agent列表失败')
}
}
// 创建
const handleCreate = () => {
dialogTitle.value = '创建Agent'
currentAgentId.value = null
form.value = {
name: '',
description: '',
workflow_config: {
nodes: [
{
id: 'start-1',
type: 'start',
position: { x: 0, y: 0 },
data: { label: '开始' }
},
{
id: 'end-1',
type: 'end',
position: { x: 200, y: 0 },
data: { label: '结束' }
}
],
edges: []
}
}
dialogVisible.value = true
}
// 编辑
const handleEdit = (agent: Agent) => {
dialogTitle.value = '编辑Agent'
currentAgentId.value = agent.id
form.value = {
name: agent.name,
description: agent.description || '',
workflow_config: agent.workflow_config
}
dialogVisible.value = true
}
// 使用Agent跳转到设计器那里有聊天界面
const handleUse = (agent: Agent) => {
router.push({
name: 'AgentDesigner',
params: { id: agent.id }
})
}
// 设计
const handleDesign = (agent: Agent) => {
router.push({
name: 'AgentDesigner',
params: { id: agent.id }
})
}
// 部署
const handleDeploy = async (agent: Agent) => {
try {
await ElMessageBox.confirm(
`确定要部署Agent "${agent.name}" 吗?`,
'确认部署',
{
type: 'warning'
}
)
await agentStore.deployAgent(agent.id)
ElMessage.success('部署成功')
await loadAgents()
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.response?.data?.detail || '部署失败')
}
}
}
// 停止
const handleStop = async (agent: Agent) => {
try {
await ElMessageBox.confirm(
`确定要停止Agent "${agent.name}" 吗?`,
'确认停止',
{
type: 'warning'
}
)
await agentStore.stopAgent(agent.id)
ElMessage.success('停止成功')
await loadAgents()
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.response?.data?.detail || '停止失败')
}
}
}
// 复制
const handleDuplicate = async (agent: Agent) => {
try {
const { value } = await ElMessageBox.prompt(
`请输入新Agent的名称留空将自动生成`,
'复制Agent',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPlaceholder: `留空将使用: ${agent.name} (副本)`,
inputValidator: (val: string) => {
if (val && val.length > 100) {
return '名称长度不能超过100个字符'
}
return true
}
}
)
await agentStore.duplicateAgent(agent.id, value || undefined)
ElMessage.success('复制成功')
await loadAgents()
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.response?.data?.detail || '复制失败')
}
}
}
// 删除
const handleDelete = async (agent: Agent) => {
try {
await ElMessageBox.confirm(
`确定要删除Agent "${agent.name}" 吗?此操作不可恢复。`,
'确认删除',
{
type: 'error'
}
)
await agentStore.deleteAgent(agent.id)
ElMessage.success('删除成功')
await loadAgents()
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.response?.data?.detail || '删除失败')
}
}
}
// 导出Agent
const handleExport = async (agent: Agent) => {
try {
await agentStore.exportAgent(agent.id)
ElMessage.success('Agent导出成功')
} catch (error: any) {
console.error('导出失败详情:', error)
const errorMessage = error.message || error.response?.data?.detail || '导出失败,请查看控制台获取详细信息'
ElMessage.error(errorMessage)
}
}
// 显示导入对话框
const handleImport = () => {
importDialogVisible.value = true
fileList.value = []
importFileContent.value = null
}
// 文件选择
const handleFileChange = (file: any) => {
const reader = new FileReader()
reader.onload = (e) => {
try {
const content = JSON.parse(e.target?.result as string)
importFileContent.value = content
} catch (error) {
ElMessage.error('文件格式错误请上传有效的JSON文件')
fileList.value = []
importFileContent.value = null
}
}
reader.readAsText(file.raw)
}
// 关闭导入对话框
const handleImportDialogClose = () => {
fileList.value = []
importFileContent.value = null
if (uploadRef.value) {
uploadRef.value.clearFiles()
}
}
// 确认导入
const handleConfirmImport = async () => {
if (!importFileContent.value) {
ElMessage.warning('请先选择要导入的文件')
return
}
importing.value = true
try {
const agent = await agentStore.importAgent(importFileContent.value)
importDialogVisible.value = false
ElMessage.success('Agent导入成功')
await loadAgents()
// 跳转到Agent设计器
router.push({
name: 'AgentDesigner',
params: { id: agent.id }
})
} catch (error: any) {
ElMessage.error(error.response?.data?.detail || '导入Agent失败')
} finally {
importing.value = false
}
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return
submitting.value = true
try {
if (currentAgentId.value) {
// 更新
await agentStore.updateAgent(currentAgentId.value, form.value)
ElMessage.success('更新成功')
} else {
// 创建
await agentStore.createAgent(form.value)
ElMessage.success('创建成功')
}
dialogVisible.value = false
await loadAgents()
} catch (error: any) {
ElMessage.error(error.response?.data?.detail || '操作失败')
} finally {
submitting.value = false
}
})
}
// 对话框关闭
const handleDialogClose = () => {
formRef.value?.resetFields()
currentAgentId.value = null
}
// 分页变化
const handlePageChange = () => {
loadAgents()
}
// 初始化
onMounted(() => {
loadAgents()
})
</script>
<style scoped>
.agents-page {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-header h2 {
margin: 0;
}
.filters {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
</style>