feat: Agent 监控与编排、仪表盘/配置页及文档更新

- agent_runtime: orchestrator、core/memory/schemas 调整
- agent_monitoring API、service、agent_llm_log 模型与 database 注册
- 前端 AgentDashboard、AgentConfig、Agents/MainLayout/路由与 AgentChat
- 文档:(红头)项目核心文档汇总、自主AI Agent改造完成情况、AI agent改造计划

Made-with: Cursor
This commit is contained in:
renjianbo
2026-05-01 19:32:59 +08:00
parent 09467568ec
commit 036f533881
21 changed files with 2601 additions and 411 deletions

View File

@@ -67,6 +67,10 @@
<el-icon><Monitor /></el-icon>
<span>系统监控</span>
</el-menu-item>
<el-menu-item index="agent-monitoring" @click="router.push('/agent-monitoring')">
<el-icon><DataAnalysis /></el-icon>
<span>Agent监控</span>
</el-menu-item>
<el-menu-item index="alert-rules" @click="router.push('/alert-rules')">
<el-icon><Bell /></el-icon>
<span>告警规则</span>
@@ -84,7 +88,7 @@
import { computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { Document, User, List, Connection, Setting, Star, Lock, Monitor, Bell, Grid } from '@element-plus/icons-vue'
import { Document, User, List, Connection, Setting, Star, Lock, Monitor, Bell, Grid, DataAnalysis } from '@element-plus/icons-vue'
const router = useRouter()
const route = useRoute()
@@ -103,6 +107,7 @@ const activeMenu = computed(() => {
if (route.path === '/permissions') return 'permissions'
if (route.path === '/template-market') return 'template-market'
if (route.path === '/monitoring') return 'monitoring'
if (route.path === '/agent-monitoring') return 'agent-monitoring'
if (route.path === '/alert-rules') return 'alert-rules'
return 'workflows'
})
@@ -129,6 +134,8 @@ const handleMenuSelect = (key: string) => {
router.push('/permissions')
} else if (key === 'monitoring') {
router.push('/monitoring')
} else if (key === 'agent-monitoring') {
router.push('/agent-monitoring')
} else if (key === 'alert-rules') {
router.push('/alert-rules')
}

View File

@@ -58,6 +58,12 @@ const router = createRouter({
component: () => import('@/views/WorkflowDesigner.vue'),
meta: { requiresAuth: true }
},
{
path: '/agents/:id/config',
name: 'agent-config',
component: () => import('@/views/AgentConfig.vue'),
meta: { requiresAuth: true }
},
{
path: '/data-sources',
name: 'data-sources',
@@ -88,6 +94,12 @@ const router = createRouter({
component: () => import('@/views/Monitoring.vue'),
meta: { requiresAuth: true }
},
{
path: '/agent-monitoring',
name: 'agent-monitoring',
component: () => import('@/views/AgentDashboard.vue'),
meta: { requiresAuth: true }
},
{
path: '/alert-rules',
name: 'alert-rules',

View File

@@ -2,110 +2,192 @@
<div class="agent-chat-page">
<div class="chat-header">
<div class="header-left">
<h2>{{ agent ? agent.name : 'AI Agent 对话' }}</h2>
<span v-if="agent" class="agent-status" :class="agent.status">{{ agent.status }}</span>
<h2>{{ chatMode === 'single' ? (agent ? agent.name : 'AI Agent 对话') : '多 Agent 编排' }}</h2>
</div>
<div class="header-actions">
<el-select
v-model="currentAgentId"
placeholder="选择 Agent"
@change="switchAgent"
style="width: 220px"
clearable
>
<el-option
v-for="a in agents"
:key="a.id"
:label="a.name"
:value="a.id"
>
<span>{{ a.name }}</span>
<span class="agent-option-desc">{{ a.description?.slice(0, 30) }}</span>
</el-option>
</el-select>
<el-button @click="clearChat" :disabled="messages.length === 0">
清空对话
</el-button>
<el-switch
v-model="chatMode"
active-value="orchestrate"
inactive-value="single"
active-text="编排"
inactive-text="单 Agent"
style="margin-right: 12px"
/>
<!-- 单 Agent 模式:选择 Agent -->
<template v-if="chatMode === 'single'">
<el-select v-model="currentAgentId" placeholder="选择 Agent" @change="switchAgent" style="width: 180px" clearable>
<el-option v-for="a in agents" :key="a.id" :label="a.name" :value="a.id">
<span>{{ a.name }}</span>
</el-option>
</el-select>
</template>
<!-- 编排模式:模式选择 + Agent 数 -->
<template v-if="chatMode === 'orchestrate'">
<el-select v-model="orchestrateMode" style="width: 130px">
<el-option label="辩论模式" value="debate" />
<el-option label="路由模式" value="route" />
<el-option label="顺序模式" value="sequential" />
</el-select>
<el-button @click="showOrchestrateEditor = true" style="margin-left: 8px">
配置 Agent ({{ orchestrateAgents.length }})
</el-button>
</template>
<el-button @click="clearChat" :disabled="messages.length === 0">清空</el-button>
</div>
</div>
<div class="chat-messages" ref="messagesRef">
<div v-if="messages.length === 0" class="chat-empty">
<el-icon :size="48"><ChatLineSquare /></el-icon>
<p>选择一个 Agent 开始对话</p>
<p v-if="chatMode === 'single'">选择一个 Agent 开始对话</p>
<p v-else>配置多个 Agent 后发送消息进行编排对话</p>
<p class="hint">Agent 可以使用内置工具帮你完成任务</p>
</div>
<div
v-for="(msg, i) in messages"
:key="i"
class="message"
:class="[msg.role, msg.status === 'error' ? 'error' : '']"
>
<div v-for="(msg, i) in messages" :key="i" class="message" :class="[msg.role, msg.status === 'error' ? 'error' : '']">
<div class="message-avatar">
<el-avatar :size="36" :icon="msg.role === 'user' ? UserFilled : Promotion" />
</div>
<div class="message-bubble">
<div class="message-text" v-html="renderMarkdown(msg.content)"></div>
<div v-if="msg.tool_calls && msg.tool_calls.length > 0" class="tool-calls">
<div class="tool-calls-header">
<el-icon><Tools /></el-icon>
工具调用 ({{ msg.tool_calls.length }})
<!-- 编排模式:显示每个 Agent 的独立输出 -->
<div v-if="msg.orchestrateResult" class="orchestrate-result">
<div class="orch-header">
<el-tag size="small" type="info">{{ msg.orchestrateResult.mode }}</el-tag>
<span class="orch-agent-count">{{ msg.orchestrateResult.steps.length }} 个 Agent</span>
</div>
<div
v-for="(tc, j) in msg.tool_calls"
:key="j"
class="tool-call-item"
>
<span class="tool-name">{{ tc.function?.name || '?' }}</span>
<el-tag size="small" type="info">
{{ Object.keys(JSON.parse(tc.function?.arguments || '{}')).length }} 个参数
</el-tag>
<div class="orch-final">
<div class="orch-section-title">最终回答</div>
<div class="message-text" v-html="renderMarkdown(msg.orchestrateResult.final_answer)"></div>
</div>
<div class="orch-steps">
<div
v-for="(step, si) in msg.orchestrateResult.steps"
:key="si"
class="orch-step"
:class="{ expanded: step._open }"
>
<div class="orch-step-header" @click="step._open = !step._open">
<el-icon><CaretRight :style="{ transform: step._open ? 'rotate(90deg)' : '' }" /></el-icon>
<el-tag size="small" :type="step.error ? 'danger' : 'success'" round>
{{ step.agent_name }}
</el-tag>
<span class="orch-step-meta">
{{ step.iterations_used }} 步 · {{ step.tool_calls_made }} 次工具
</span>
</div>
<div v-show="step._open" class="orch-step-body">
<div class="message-text" v-html="renderMarkdown(step.output)"></div>
</div>
</div>
</div>
</div>
<!-- 单 Agent 模式:原有内容 -->
<template v-if="!msg.orchestrateResult">
<div class="message-text" v-html="renderMarkdown(msg.content)"></div>
<div v-if="msg.tool_calls && msg.tool_calls.length > 0" class="tool-calls">
<div class="tool-calls-header">
<el-icon><Tools /></el-icon> 工具调用 ({{ msg.tool_calls.length }})
</div>
<div v-for="(tc, j) in msg.tool_calls" :key="j" class="tool-call-item">
<span class="tool-name">{{ tc.function?.name || '?' }}</span>
<el-tag size="small" type="info">{{ Object.keys(JSON.parse(tc.function?.arguments || '{}')).length }} 个参数</el-tag>
</div>
</div>
<!-- 思考链 -->
<div v-if="msg.steps && msg.steps.length > 0" class="thinking-trace">
<div class="trace-header" @click="toggleTrace(msg)">
<el-icon><CaretRight :style="{ transform: msg._traceOpen ? 'rotate(90deg)' : '' }" /></el-icon>
<span>思考链 ({{ msg.steps.length }} 步)</span>
</div>
<div v-show="msg._traceOpen" class="trace-steps">
<div v-for="(step, si) in msg.steps" :key="si" class="trace-step" :class="'step-' + step.type">
<div class="step-icon">
<el-icon v-if="step.type === 'think'"><ChatDotSquare /></el-icon>
<el-icon v-else-if="step.type === 'tool_result'"><Tools /></el-icon>
<el-icon v-else><Select /></el-icon>
</div>
<div class="step-body">
<div class="step-header">
<span class="step-tag" :class="'tag-' + step.type">{{ {think:'思考',tool_result:'工具结果',final:'最终回答'}[step.type] || step.type }}</span>
<span class="step-iter">#{{ step.iteration }}</span>
<span v-if="step.tool_name" class="step-tool-name">{{ step.tool_name }}</span>
</div>
<div v-if="step.content" class="step-content" v-html="renderMarkdown(step.content)"></div>
<div v-if="step.reasoning" class="step-reasoning">
<div class="reasoning-header">推理过程</div>
<div class="reasoning-text">{{ step.reasoning }}</div>
</div>
<div v-if="step.tool_input && Object.keys(step.tool_input).length" class="step-tool-input">
<div class="reasoning-header">参数</div>
<pre>{{ JSON.stringify(step.tool_input, null, 2) }}</pre>
</div>
<div v-if="step.tool_result" class="step-tool-result">
<div class="reasoning-header">结果</div>
<pre>{{ step.tool_result }}</pre>
</div>
</div>
</div>
</div>
</div>
</template>
<div class="message-meta">
{{ msg.role === 'user' ? '用户' : 'Agent' }} ·
{{ formatTime(msg.timestamp) }}
<span v-if="msg.iterations" class="meta-iterations">
· {{ msg.iterations }} 步 · {{ msg.tool_calls_made }} 次工具调用
</span>
{{ msg.role === 'user' ? '用户' : 'Agent' }} · {{ formatTime(msg.timestamp) }}
<span v-if="msg.iterations" class="meta-iterations">· {{ msg.iterations }} 步 · {{ msg.tool_calls_made }} 次工具调用</span>
</div>
</div>
</div>
<div v-if="loading" class="message assistant">
<div class="message-avatar">
<el-avatar :size="36" icon="Promotion" />
</div>
<div class="message-avatar"><el-avatar :size="36" icon="Promotion" /></div>
<div class="message-bubble">
<div class="thinking">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
<div class="thinking"><span class="dot"></span><span class="dot"></span><span class="dot"></span></div>
</div>
</div>
</div>
<div class="chat-input">
<el-input
v-model="inputMessage"
type="textarea"
:rows="3"
placeholder="输入你的问题,Agent 会自动使用工具来帮助你..."
@keydown.enter.exact.prevent="sendMessage"
:disabled="loading"
/>
<el-button
type="primary"
@click="sendMessage"
:loading="loading"
:disabled="!inputMessage.trim()"
class="send-btn"
>
{{ loading ? '思考中...' : '发送' }}
<el-input v-model="inputMessage" type="textarea" :rows="3" placeholder="输入你的问题..." @keydown.enter.exact.prevent="sendMessage" :disabled="loading" />
<el-button type="primary" @click="sendMessage" :loading="loading" :disabled="!inputMessage.trim()" class="send-btn">
{{ loading ? '处理中...' : '发送' }}
</el-button>
</div>
<!-- 编排 Agent 编辑器 -->
<el-dialog v-model="showOrchestrateEditor" title="编排 Agent 配置" width="700px">
<div class="orch-editor">
<div v-for="(agt, i) in orchestrateAgents" :key="i" class="orch-agent-card">
<div class="orch-agent-header">
<span class="orch-agent-num">#{{ i + 1 }}</span>
<el-input v-model="agt.name" placeholder="名称" style="width: 140px" size="small" />
<el-input v-model="agt.id" placeholder="ID" style="width: 120px" size="small" />
<el-button size="small" type="danger" link @click="orchestrateAgents.splice(i, 1)">删除</el-button>
</div>
<el-input v-model="agt.system_prompt" type="textarea" :rows="3" placeholder="System Prompt" size="small" />
<div class="orch-agent-params">
<el-select v-model="agt.model" size="small" style="width: 160px">
<el-option label="DeepSeek V4 Flash" value="deepseek-v4-flash" />
<el-option label="DeepSeek V4 Pro" value="deepseek-v4-pro" />
<el-option label="GPT-4o Mini" value="gpt-4o-mini" />
<el-option label="GPT-4o" value="gpt-4o" />
</el-select>
<el-input-number v-model="agt.temperature" :min="0" :max="2" :step="0.1" size="small" style="width: 110px" />
<el-input-number v-model="agt.max_iterations" :min="1" :max="50" size="small" style="width: 110px" />
</div>
</div>
<el-button @click="addOrchestrateAgent" style="width: 100%; margin-top: 8px">
+ 添加 Agent
</el-button>
</div>
<template #footer>
<el-button @click="showOrchestrateEditor = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -113,23 +195,30 @@
import { ref, onMounted, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import {
ChatLineSquare,
UserFilled,
Promotion,
Tools,
} from '@element-plus/icons-vue'
import { ChatLineSquare, UserFilled, Promotion, Tools, CaretRight, ChatDotSquare, Select } from '@element-plus/icons-vue'
import api from '@/api'
import type { Agent } from '@/stores/agent'
interface AgentStep {
iteration: number; type: string; content: string
tool_name?: string; tool_input?: Record<string, any>; tool_result?: string; reasoning?: string
}
interface OrchestrateStep {
agent_id: string; agent_name: string; input: string; output: string
iterations_used: number; tool_calls_made: number; error?: string; _open?: boolean
}
interface OrchestrateResult {
mode: string; final_answer: string; steps: OrchestrateStep[]; agent_results: any[]
}
interface ChatMessage {
role: 'user' | 'assistant'
content: string
tool_calls?: any[]
timestamp: number
iterations?: number
tool_calls_made?: number
status?: string
role: 'user' | 'assistant'; content: string; tool_calls?: any[]; timestamp: number
iterations?: number; tool_calls_made?: number; status?: string; steps?: AgentStep[]
_traceOpen?: boolean; orchestrateResult?: OrchestrateResult
}
interface OrchestrateAgentForm {
id: string; name: string; system_prompt: string; model: string
temperature: number; max_iterations: number; description: string
}
const route = useRoute()
@@ -140,9 +229,30 @@ const inputMessage = ref('')
const loading = ref(false)
const messagesRef = ref<HTMLElement | null>(null)
const sessionId = ref('')
const agent = ref<Agent | null>(null)
// 编排模式
const chatMode = ref<'single' | 'orchestrate'>('single')
const orchestrateMode = ref('debate')
const showOrchestrateEditor = ref(false)
const orchestrateAgents = ref<OrchestrateAgentForm[]>([
{ id: 'agent-a', name: 'Agent A', system_prompt: '你是一个有用的AI助手。', model: 'deepseek-v4-flash', temperature: 0.7, max_iterations: 10, description: '' },
{ id: 'agent-b', name: 'Agent B', system_prompt: '你是一个专业的分析助手。', model: 'deepseek-v4-flash', temperature: 0.7, max_iterations: 10, description: '' },
])
function addOrchestrateAgent() {
const n = orchestrateAgents.value.length + 1
orchestrateAgents.value.push({
id: `agent-${String.fromCharCode(96 + n)}`,
name: `Agent ${String.fromCharCode(64 + n)}`,
system_prompt: '你是一个有用的AI助手。',
model: 'deepseek-v4-flash',
temperature: 0.7,
max_iterations: 10,
description: '',
})
}
onMounted(async () => {
await loadAgents()
if (route.params.id) {
@@ -152,302 +262,158 @@ onMounted(async () => {
})
async function loadAgents() {
try {
const resp = await api.get('/api/v1/agents')
agents.value = resp.data || []
} catch (e) {
console.error('加载 Agent 列表失败:', e)
}
try { const resp = await api.get('/api/v1/agents'); agents.value = resp.data || [] }
catch (e) { console.error('加载 Agent 列表失败:', e) }
}
async function switchAgent() {
if (!currentAgentId.value) {
agent.value = null
return
}
try {
const resp = await api.get(`/api/v1/agents/${currentAgentId.value}`)
agent.value = resp.data
} catch (e: any) {
ElMessage.error('加载 Agent 失败')
agent.value = null
}
if (!currentAgentId.value) { agent.value = null; return }
try { const resp = await api.get(`/api/v1/agents/${currentAgentId.value}`); agent.value = resp.data }
catch { ElMessage.error('加载 Agent 失败'); agent.value = null }
}
async function sendMessage() {
const text = inputMessage.value.trim()
if (!text || loading.value) return
messages.value.push({
role: 'user',
content: text,
timestamp: Date.now(),
})
messages.value.push({ role: 'user', content: text, timestamp: Date.now() })
inputMessage.value = ''
loading.value = true
scrollToBottom()
try {
const endpoint = currentAgentId.value
? `/api/v1/agent-chat/${currentAgentId.value}`
: '/api/v1/agent-chat/bare'
const resp = await api.post(endpoint, {
message: text,
session_id: sessionId.value || undefined,
})
const data = resp.data
sessionId.value = data.session_id
messages.value.push({
role: 'assistant',
content: data.content,
timestamp: Date.now(),
iterations: data.iterations_used,
tool_calls_made: data.tool_calls_made,
status: data.truncated ? 'error' : 'success',
})
if (chatMode.value === 'orchestrate') {
const resp = await api.post('/api/v1/agent-chat/orchestrate', {
message: text,
mode: orchestrateMode.value,
agents: orchestrateAgents.value.map(a => ({
id: a.id, name: a.name, system_prompt: a.system_prompt,
model: a.model, temperature: a.temperature, max_iterations: a.max_iterations,
tools: [], description: a.description,
})),
})
const data = resp.data as OrchestrateResult
data.steps.forEach(s => { s._open = false })
messages.value.push({
role: 'assistant', content: data.final_answer, timestamp: Date.now(),
orchestrateResult: data, _traceOpen: true,
})
} else {
const endpoint = currentAgentId.value ? `/api/v1/agent-chat/${currentAgentId.value}` : '/api/v1/agent-chat/bare'
const resp = await api.post(endpoint, { message: text, session_id: sessionId.value || undefined })
const data = resp.data
sessionId.value = data.session_id
messages.value.push({
role: 'assistant', content: data.content, timestamp: Date.now(),
iterations: data.iterations_used, tool_calls_made: data.tool_calls_made,
status: data.truncated ? 'error' : 'success', steps: data.steps || [],
_traceOpen: data.steps && data.steps.length > 0,
})
}
} catch (e: any) {
messages.value.push({
role: 'assistant',
content: `错误:${e.response?.data?.detail || e.message || '请求失败'}`,
timestamp: Date.now(),
status: 'error',
role: 'assistant', content: `错误:${e.response?.data?.detail || e.message || '请求失败'}`,
timestamp: Date.now(), status: 'error',
})
} finally {
loading.value = false
scrollToBottom()
loading.value = false; scrollToBottom()
}
}
function clearChat() {
messages.value = []
sessionId.value = ''
}
function scrollToBottom() {
nextTick(() => {
if (messagesRef.value) {
messagesRef.value.scrollTop = messagesRef.value.scrollHeight
}
})
}
function formatTime(ts: number) {
return new Date(ts).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
})
}
function toggleTrace(msg: ChatMessage) { msg._traceOpen = !msg._traceOpen }
function clearChat() { messages.value = []; sessionId.value = '' }
function scrollToBottom() { nextTick(() => { if (messagesRef.value) messagesRef.value.scrollTop = messagesRef.value.scrollHeight }) }
function formatTime(ts: number) { return new Date(ts).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) }
function renderMarkdown(text: string): string {
if (!text) return ''
// 简单的 Markdown 渲染(代码块、加粗、链接)
let html = text
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
// 代码块
return text.replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code class="language-$1">$2</code></pre>')
// 行内代码
.replace(/`([^`]+)`/g, '<code>$1</code>')
// 加粗
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
// 换行
.replace(/\n/g, '<br>')
return html
}
</script>
<style scoped>
.agent-chat-page {
display: flex;
flex-direction: column;
height: calc(100vh - 120px);
max-width: 900px;
margin: 0 auto;
padding: 16px;
}
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--el-border-color-light);
}
.header-left {
display: flex;
align-items: center;
gap: 8px;
}
.header-left h2 {
margin: 0;
font-size: 18px;
}
.header-actions {
display: flex;
gap: 8px;
align-items: center;
}
.agent-status {
font-size: 12px;
padding: 2px 8px;
border-radius: 10px;
background: var(--el-color-info-light-8);
}
.agent-status.published { background: var(--el-color-success-light-8); color: var(--el-color-success); }
.agent-status.draft { background: var(--el-color-warning-light-8); color: var(--el-color-warning); }
.agent-option-desc {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-left: 8px;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 12px 0;
display: flex;
flex-direction: column;
gap: 16px;
}
.chat-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--el-text-color-secondary);
gap: 12px;
}
.chat-empty .hint {
font-size: 13px;
color: var(--el-text-color-placeholder);
}
.message {
display: flex;
gap: 12px;
max-width: 85%;
}
.agent-chat-page { display: flex; flex-direction: column; height: calc(100vh - 120px); max-width: 960px; margin: 0 auto; padding: 16px; }
.chat-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--el-border-color-light); }
.header-left { display: flex; align-items: center; gap: 8px; }
.header-left h2 { margin: 0; font-size: 18px; }
.header-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.chat-messages { flex: 1; overflow-y: auto; padding: 12px 0; display: flex; flex-direction: column; gap: 16px; }
.chat-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--el-text-color-secondary); gap: 12px; }
.chat-empty .hint { font-size: 13px; color: var(--el-text-color-placeholder); }
.message { display: flex; gap: 12px; max-width: 88%; }
.message.user { align-self: flex-end; flex-direction: row-reverse; }
.message.assistant { align-self: flex-start; }
.message-bubble { padding: 10px 14px; border-radius: 12px; background: var(--el-fill-color-light); line-height: 1.6; font-size: 14px; }
.message.user .message-bubble { background: var(--el-color-primary-light-8); }
.message.error .message-bubble { border: 1px solid var(--el-color-danger-light-5); }
.message-text :deep(pre) { background: var(--el-fill-color); padding: 12px; border-radius: 8px; overflow-x: auto; font-size: 13px; }
.message-text :deep(code) { background: var(--el-fill-color); padding: 2px 6px; border-radius: 4px; font-size: 13px; }
.message-bubble {
padding: 10px 14px;
border-radius: 12px;
background: var(--el-fill-color-light);
line-height: 1.6;
font-size: 14px;
}
/* Tool calls */
.tool-calls { margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--el-border-color-light); }
.tool-calls-header { display: flex; align-items: center; gap: 4px; font-size: 12px; color: var(--el-text-color-secondary); margin-bottom: 4px; }
.tool-call-item { display: flex; align-items: center; gap: 8px; padding: 4px 8px; font-size: 12px; }
.tool-name { font-weight: 500; color: var(--el-color-primary); }
.message-meta { font-size: 11px; color: var(--el-text-color-placeholder); margin-top: 4px; }
.meta-iterations { color: var(--el-color-info); }
.message.user .message-bubble {
background: var(--el-color-primary-light-8);
}
.message.error .message-bubble {
border: 1px solid var(--el-color-danger-light-5);
}
.message-text :deep(pre) {
background: var(--el-fill-color);
padding: 12px;
border-radius: 8px;
overflow-x: auto;
font-size: 13px;
}
.message-text :deep(code) {
background: var(--el-fill-color);
padding: 2px 6px;
border-radius: 4px;
font-size: 13px;
}
.tool-calls {
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed var(--el-border-color-light);
}
.tool-calls-header {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--el-text-color-secondary);
margin-bottom: 4px;
}
.tool-call-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
font-size: 12px;
}
.tool-name {
font-weight: 500;
color: var(--el-color-primary);
}
.message-meta {
font-size: 11px;
color: var(--el-text-color-placeholder);
margin-top: 4px;
}
.meta-iterations {
color: var(--el-color-info);
}
.thinking {
display: flex;
gap: 4px;
padding: 8px 0;
}
.dot {
width: 8px;
height: 8px;
background: var(--el-text-color-placeholder);
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out;
}
/* Thinking trace */
.thinking-trace { margin-top: 10px; border-top: 1px solid var(--el-border-color-light); padding-top: 8px; }
.trace-header { display: flex; align-items: center; gap: 4px; cursor: pointer; font-size: 12px; color: var(--el-color-primary); user-select: none; padding: 4px 0; }
.trace-steps { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
.trace-step { display: flex; gap: 8px; padding: 8px 10px; border-radius: 8px; background: var(--el-fill-color-lighter); border-left: 3px solid var(--el-border-color); }
.trace-step.step-think { border-left-color: var(--el-color-primary); }
.trace-step.step-tool_result { border-left-color: var(--el-color-warning); }
.trace-step.step-final { border-left-color: var(--el-color-success); }
.step-icon { flex-shrink: 0; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; font-size: 14px; color: var(--el-text-color-secondary); }
.step-body { flex: 1; min-width: 0; font-size: 13px; }
.step-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
.step-tag { font-size: 11px; padding: 1px 6px; border-radius: 4px; font-weight: 500; }
.tag-think { background: var(--el-color-primary-light-9); color: var(--el-color-primary); }
.tag-tool_result { background: var(--el-color-warning-light-9); color: var(--el-color-warning); }
.tag-final { background: var(--el-color-success-light-9); color: var(--el-color-success); }
.step-iter { font-size: 11px; color: var(--el-text-color-placeholder); }
.step-tool-name { font-size: 11px; background: var(--el-color-info-light-9); color: var(--el-color-info); padding: 0 6px; border-radius: 4px; font-family: monospace; }
.step-content { line-height: 1.5; }
.step-content :deep(pre) { background: var(--el-fill-color); padding: 8px; border-radius: 6px; overflow-x: auto; font-size: 12px; margin: 4px 0; }
.step-reasoning, .step-tool-input, .step-tool-result { margin-top: 6px; }
.reasoning-header { font-size: 11px; color: var(--el-text-color-secondary); margin-bottom: 2px; font-weight: 500; }
.reasoning-text { font-size: 12px; color: var(--el-text-color-secondary); line-height: 1.5; font-style: italic; }
.step-tool-input pre, .step-tool-result pre { background: var(--el-fill-color-darker); padding: 6px 8px; border-radius: 4px; font-size: 11px; overflow-x: auto; max-height: 200px; margin: 0; }
/* Thinking dots */
.thinking { display: flex; gap: 4px; padding: 8px 0; }
.dot { width: 8px; height: 8px; background: var(--el-text-color-placeholder); border-radius: 50%; animation: bounce 1.4s infinite ease-in-out; }
.dot:nth-child(2) { animation-delay: 0.16s; }
.dot:nth-child(3) { animation-delay: 0.32s; }
@keyframes bounce { 0%,80%,100% { transform: scale(0); } 40% { transform: scale(1); } }
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
/* Chat input */
.chat-input { display: flex; gap: 12px; padding-top: 12px; border-top: 1px solid var(--el-border-color-light); }
.chat-input .el-textarea { flex: 1; }
.send-btn { align-self: flex-end; min-width: 100px; }
.chat-input {
display: flex;
gap: 12px;
padding-top: 12px;
border-top: 1px solid var(--el-border-color-light);
}
/* Orchestrate result */
.orchestrate-result { font-size: 14px; }
.orch-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.orch-agent-count { font-size: 12px; color: var(--el-text-color-secondary); }
.orch-final { margin-bottom: 12px; }
.orch-section-title { font-size: 13px; font-weight: 600; color: var(--el-text-color-primary); margin-bottom: 6px; padding-bottom: 4px; border-bottom: 1px solid var(--el-border-color-light); }
.orch-steps { display: flex; flex-direction: column; gap: 4px; }
.orch-step { border: 1px solid var(--el-border-color-lighter); border-radius: 8px; overflow: hidden; }
.orch-step-header { display: flex; align-items: center; gap: 6px; padding: 6px 10px; cursor: pointer; background: var(--el-fill-color-lighter); font-size: 13px; }
.orch-step-header:hover { background: var(--el-fill-color-light); }
.orch-step-meta { font-size: 11px; color: var(--el-text-color-placeholder); margin-left: auto; }
.orch-step-body { padding: 8px 12px; font-size: 13px; background: var(--el-bg-color); }
.chat-input .el-textarea {
flex: 1;
}
.send-btn {
align-self: flex-end;
min-width: 100px;
}
/* Orchestrate editor */
.orch-editor { display: flex; flex-direction: column; gap: 12px; max-height: 500px; overflow-y: auto; }
.orch-agent-card { border: 1px solid var(--el-border-color-light); border-radius: 8px; padding: 12px; }
.orch-agent-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.orch-agent-num { font-weight: 600; color: var(--el-color-primary); font-size: 14px; }
.orch-agent-params { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; }
</style>

View File

@@ -0,0 +1,306 @@
<template>
<MainLayout>
<div class="agent-config-page">
<el-card v-loading="loading">
<template #header>
<div class="card-header">
<div class="header-left">
<el-button @click="goBack" text>
<el-icon><ArrowLeft /></el-icon>
</el-button>
<h2>Agent 配置:{{ agent?.name || '加载中...' }}</h2>
</div>
<el-button type="primary" @click="handleSave" :loading="saving">
保存配置
</el-button>
</div>
</template>
<el-form label-position="top" class="config-form">
<!-- System Prompt -->
<el-form-item label="系统提示词 (System Prompt)">
<el-input
v-model="form.system_prompt"
type="textarea"
:rows="6"
placeholder="设置 Agent 的角色和行为指令..."
/>
<div class="form-tip">该提示词决定了 Agent 的行为方式和专业领域</div>
</el-form-item>
<!-- 模型选择 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="模型">
<el-select v-model="form.model" placeholder="选择模型" filterable style="width: 100%">
<el-option-group label="模型配置">
<el-option
v-for="mc in modelConfigs"
:key="mc.id"
:label="`${mc.name} (${mc.model_name})`"
:value="mc.model_name"
>
<span>{{ mc.name }}</span>
<span class="option-detail">{{ mc.provider }} / {{ mc.model_name }}</span>
</el-option>
</el-option-group>
<el-option-group label="常用模型">
<el-option label="DeepSeek V4 Flash" value="deepseek-v4-flash" />
<el-option label="DeepSeek V4 Pro" value="deepseek-v4-pro" />
<el-option label="GPT-4o Mini" value="gpt-4o-mini" />
<el-option label="GPT-4o" value="gpt-4o" />
</el-option-group>
</el-select>
<div class="form-tip">选择 Agent 使用的 AI 模型</div>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="Temperature">
<el-slider
v-model="form.temperature"
:min="0"
:max="2"
:step="0.1"
show-input
input-size="small"
/>
<div class="form-tip">越低越确定,越高越有创造性</div>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="Provider">
<el-select v-model="form.provider" placeholder="提供商" style="width: 100%">
<el-option label="OpenAI" value="openai" />
<el-option label="DeepSeek" value="deepseek" />
<el-option label="Anthropic" value="anthropic" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 迭代次数 & 记忆 -->
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="最大迭代次数 (Max Iterations)">
<el-input-number
v-model="form.max_iterations"
:min="1"
:max="50"
style="width: 100%"
/>
<div class="form-tip">Agent 自主思考-行动循环的最大步数</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="长期记忆">
<el-switch v-model="form.memory_enabled" active-text="启用" inactive-text="禁用" />
<div class="form-tip">启用后 Agent 会记住对话历史中的关键信息</div>
</el-form-item>
</el-col>
</el-row>
<!-- 工具选择 -->
<el-form-item label="可用工具">
<el-checkbox-group v-model="form.tools" class="tool-checkbox-group">
<el-checkbox
v-for="tool in availableTools"
:key="tool.name"
:label="tool.name"
border
>
{{ tool.label }}
<span class="tool-name">({{ tool.name }})</span>
</el-checkbox>
</el-checkbox-group>
<div class="form-tip">勾选的工具 Agent 可在运行时自主调用,不勾选则使用全部</div>
</el-form-item>
</el-form>
</el-card>
</div>
</MainLayout>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
import MainLayout from '@/components/MainLayout.vue'
import { useAgentStore } from '@/stores/agent'
import { useModelConfigStore } from '@/stores/modelConfig'
import { BUILTIN_SKILL_OPTIONS } from '@/utils/agentSkills'
const route = useRoute()
const router = useRouter()
const agentStore = useAgentStore()
const modelConfigStore = useModelConfigStore()
const loading = ref(false)
const saving = ref(false)
const agent = ref(agentStore.currentAgent)
const modelConfigs = ref(modelConfigStore.modelConfigs)
const availableTools = BUILTIN_SKILL_OPTIONS
const form = ref({
system_prompt: '你是一个有用的AI助手。请使用可用工具来帮助用户完成任务。',
model: 'deepseek-v4-flash',
provider: 'deepseek',
temperature: 0.7,
max_iterations: 10,
tools: [] as string[],
memory_enabled: true,
})
onMounted(async () => {
const agentId = route.params.id as string
if (!agentId) return
loading.value = true
try {
// 加载 Agent 详情
const a = await agentStore.fetchAgent(agentId)
agent.value = a
// 从工作流配置中提取已有设置
const nodes = a.workflow_config?.nodes || []
const agentNode = findAgentNode(nodes)
const data = agentNode?.data || {}
form.value.system_prompt = data.system_prompt || form.value.system_prompt
form.value.model = data.model || form.value.model
form.value.provider = data.provider || form.value.provider
form.value.temperature = data.temperature ?? form.value.temperature
form.value.max_iterations = data.max_iterations ?? form.value.max_iterations
form.value.tools = Array.isArray(data.tools) ? [...data.tools] : []
form.value.memory_enabled = data.memory !== false
} catch (e: any) {
ElMessage.error('加载 Agent 失败')
router.push('/agents')
} finally {
loading.value = false
}
// 加载模型配置列表
try {
await modelConfigStore.fetchModelConfigs()
modelConfigs.value = modelConfigStore.modelConfigs
} catch {
// 可选:模型配置加载失败不影响配置页
}
})
interface WorkflowNodeData {
[key: string]: unknown
}
interface WorkflowNode {
id: string
type: string
data?: WorkflowNodeData
position?: { x: number; y: number }
}
function findAgentNode(nodes: WorkflowNode[]): WorkflowNode | undefined {
return nodes.find((n) => n.type === 'agent' || n.type === 'llm')
}
function goBack() {
router.push('/agents')
}
async function handleSave() {
if (!agent.value?.id) return
saving.value = true
try {
// 读取当前 workflow_config,更新或创建 agent 节点数据
const wf = JSON.parse(JSON.stringify(agent.value.workflow_config || { nodes: [], edges: [] }))
const nodes: WorkflowNode[] = wf.nodes || []
let targetNode = findAgentNode(nodes)
if (!targetNode) {
// 没有 agent 节点,新建一个
targetNode = {
id: 'agent-config-1',
type: 'agent',
position: { x: 250, y: 250 },
data: {},
}
nodes.push(targetNode)
}
if (!targetNode.data) targetNode.data = {}
targetNode.data.system_prompt = form.value.system_prompt
targetNode.data.model = form.value.model
targetNode.data.provider = form.value.provider
targetNode.data.temperature = form.value.temperature
targetNode.data.max_iterations = form.value.max_iterations
targetNode.data.tools = [...form.value.tools]
targetNode.data.selected_tools = [...form.value.tools]
targetNode.data.enable_tools = form.value.tools.length > 0
targetNode.data.memory = form.value.memory_enabled
wf.nodes = nodes
await agentStore.updateAgent(agent.value.id, { workflow_config: wf })
ElMessage.success('配置已保存')
} catch (e: any) {
ElMessage.error(e.response?.data?.detail || '保存失败')
} finally {
saving.value = false
}
}
</script>
<style scoped>
.agent-config-page {
padding: 20px;
max-width: 960px;
margin: 0 auto;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left {
display: flex;
align-items: center;
gap: 8px;
}
.header-left h2 {
margin: 0;
font-size: 18px;
}
.config-form {
margin-top: 8px;
}
.form-tip {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-top: 4px;
line-height: 1.4;
}
.tool-checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tool-name {
color: var(--el-text-color-secondary);
font-size: 12px;
margin-left: 4px;
}
.option-detail {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-left: 8px;
}
</style>

View File

@@ -0,0 +1,441 @@
<template>
<MainLayout>
<div class="agent-dashboard-page">
<el-card>
<template #header>
<div class="card-header">
<h2>Agent 监控</h2>
<div class="header-actions">
<el-select v-model="days" style="width: 140px; margin-right: 12px;">
<el-option :value="1" label="最近1天" />
<el-option :value="7" label="最近7天" />
<el-option :value="14" label="最近14天" />
<el-option :value="30" label="最近30天" />
</el-select>
<el-button @click="refreshData" :loading="loading">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
</template>
<!-- 概览卡片 -->
<div class="overview-section">
<el-row :gutter="20">
<el-col :span="4" v-for="item in overviewItems" :key="item.key">
<el-card class="stat-card" shadow="hover">
<div class="stat-value" :style="{ color: item.color }">{{ item.value }}</div>
<div class="stat-label">{{ item.label }}</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- 日趋势图 -->
<div class="trend-section">
<h3>日 LLM 调用趋势(最近{{ days }}天)</h3>
<el-row :gutter="20">
<el-col :span="16">
<el-card>
<div class="trend-chart">
<div
v-for="(item, index) in dailyTrend"
:key="index"
class="trend-bar"
:style="{ height: `${(item.call_count / maxDailyCount) * 180}px` }"
:title="`${item.date}: ${item.call_count}次`"
>
<div class="trend-value">{{ item.call_count }}</div>
</div>
</div>
<div class="trend-labels">
<span v-for="(item, index) in dailyTrend" :key="index" class="trend-label">
{{ item.date.slice(-2) + '日' }}
</span>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card>
<h4>Token 用量</h4>
<div class="token-summary">
<div class="token-item">
<span class="label">Prompt Tokens</span>
<span class="value">{{ formatTokens(overview.total_prompt_tokens) }}</span>
</div>
<div class="token-item">
<span class="label">Completion Tokens</span>
<span class="value">{{ formatTokens(overview.total_completion_tokens) }}</span>
</div>
<div class="token-item">
<span class="label">总 Tokens</span>
<span class="value total">{{ formatTokens(overview.total_tokens) }}</span>
</div>
<el-divider />
<div class="token-item">
<span class="label">LLM 调用次数</span>
<span class="value">{{ overview.llm_call_count }}</span>
</div>
<div class="token-item">
<span class="label">工具调用次数</span>
<span class="value">{{ overview.tool_call_count }}</span>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- Agent 用量排行 -->
<div class="section">
<h3>Agent 用量排行(最近{{ days }}天)</h3>
<el-card>
<el-table :data="agentStats" border stripe empty-text="暂无数据">
<el-table-column prop="agent_name" label="Agent" min-width="160" />
<el-table-column prop="call_count" label="LLM 调用" width="110" align="center" sortable />
<el-table-column prop="total_tokens" label="总 Tokens" width="120" align="center" sortable>
<template #default="{ row }">
{{ formatTokens(row.total_tokens) }}
</template>
</el-table-column>
<el-table-column prop="tool_call_count" label="工具调用" width="110" align="center" sortable />
<el-table-column prop="avg_latency_ms" label="平均延迟" width="120" align="center" sortable>
<template #default="{ row }">
{{ formatLatency(row.avg_latency_ms) }}
</template>
</el-table-column>
</el-table>
</el-card>
</div>
<!-- 工具调用频次 -->
<div class="section">
<h3>工具调用频次(最近{{ days }}天)</h3>
<el-card>
<el-table :data="toolUsage" border stripe empty-text="暂无数据">
<el-table-column prop="tool_name" label="工具名称" min-width="160" />
<el-table-column prop="call_count" label="调用次数" width="120" align="center" sortable />
<el-table-column prop="total_tokens" label="消耗 Tokens" width="140" align="center" sortable>
<template #default="{ row }">
{{ formatTokens(row.total_tokens) }}
</template>
</el-table-column>
<el-table-column prop="avg_latency_ms" label="平均延迟" width="140" align="center" sortable>
<template #default="{ row }">
{{ formatLatency(row.avg_latency_ms) }}
</template>
</el-table-column>
</el-table>
</el-card>
</div>
<!-- 最近 LLM 调用记录 -->
<div class="section">
<h3>最近 LLM 调用记录(最近{{ days }}天)</h3>
<el-card>
<el-table :data="llmCalls" border stripe empty-text="暂无 LLM 调用记录" max-height="400">
<el-table-column prop="created_at" label="时间" width="170">
<template #default="{ row }">
{{ formatDateTime(row.created_at) }}
</template>
</el-table-column>
<el-table-column prop="model" label="模型" width="160" />
<el-table-column prop="total_tokens" label="Tokens" width="90" align="center">
<template #default="{ row }">
<el-tag size="small" effect="plain">{{ row.total_tokens || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="latency_ms" label="耗时" width="90" align="center">
<template #default="{ row }">
{{ formatLatency(row.latency_ms) }}
</template>
</el-table-column>
<el-table-column prop="iteration_number" label="轮次" width="70" align="center" />
<el-table-column prop="step_type" label="类型" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.step_type === 'final' ? 'success' : 'primary'" size="small" effect="plain">
{{ row.step_type === 'final' ? '回答' : '思考' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="tool_name" label="工具" width="120">
<template #default="{ row }">
<el-tag v-if="row.tool_name" type="warning" size="small">{{ row.tool_name }}</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 'success' ? 'success' : 'danger'" size="small">
{{ row.status === 'success' ? '成功' : '失败' }}
</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</el-card>
</div>
</MainLayout>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import MainLayout from '@/components/MainLayout.vue'
import api from '@/api'
const loading = ref(false)
const days = ref(7)
// 概览
const overview = ref({
agent_count: 0,
chat_count: 0,
llm_call_count: 0,
total_prompt_tokens: 0,
total_completion_tokens: 0,
total_tokens: 0,
tool_call_count: 0,
})
// Agent 用量
const agentStats = ref([] as Array<{
agent_name: string
call_count: number
total_tokens: number
tool_call_count: number
avg_latency_ms: number
}>)
// 工具使用
const toolUsage = ref([] as Array<{
tool_name: string
call_count: number
total_tokens: number
avg_latency_ms: number
}>)
// LLM 调用记录
const llmCalls = ref([] as Array<{
created_at: string
model: string
total_tokens: number
latency_ms: number
iteration_number: number
step_type: string
tool_name: string | null
status: string
}>)
// 日趋势
const dailyTrend = ref([] as Array<{
date: string
call_count: number
total_tokens: number
}>)
// 最大日调用数(用于图表)
const maxDailyCount = computed(() => {
if (dailyTrend.value.length === 0) return 1
return Math.max(...dailyTrend.value.map(t => t.call_count), 1)
})
// 概览卡片项
const overviewItems = computed(() => [
{ key: 'agents', label: 'Agent 数', value: overview.value.agent_count, color: '#409eff' },
{ key: 'chats', label: '对话次数', value: overview.value.chat_count, color: '#67c23a' },
{ key: 'llm_calls', label: 'LLM 调用', value: overview.value.llm_call_count, color: '#e6a23c' },
{ key: 'tokens', label: '总 Tokens', value: formatTokens(overview.value.total_tokens), color: '#f56c6c' },
{ key: 'tool_calls', label: '工具调用', value: overview.value.tool_call_count, color: '#909399' },
])
// 格式化 Token 数
function formatTokens(tokens: number): string {
if (!tokens || tokens === 0) return '0'
if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(1)}M`
if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}K`
return String(tokens)
}
// 格式化延迟
function formatLatency(ms: number): string {
if (!ms || ms === 0) return '-'
if (ms < 1000) return `${ms}ms`
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
return `${(ms / 60000).toFixed(1)}min`
}
// 格式化时间
function formatDateTime(dateStr: string): string {
if (!dateStr) return ''
const date = new Date(dateStr)
return date.toLocaleString('zh-CN')
}
// 加载数据
async function loadData() {
loading.value = true
try {
const [overviewRes, agentRes, toolRes, llmRes, trendRes] = await Promise.all([
api.get('/api/v1/agent-monitoring/overview'),
api.get(`/api/v1/agent-monitoring/agents-stats?days=${days.value}`),
api.get(`/api/v1/agent-monitoring/tool-usage?days=${days.value}`),
api.get(`/api/v1/agent-monitoring/llm-calls?days=${days.value}&limit=100`),
api.get(`/api/v1/agent-monitoring/daily-trend?days=${days.value}`),
])
overview.value = overviewRes.data
agentStats.value = agentRes.data
toolUsage.value = toolRes.data
llmCalls.value = llmRes.data
dailyTrend.value = trendRes.data
} catch (error: any) {
ElMessage.error(error.response?.data?.detail || '加载 Agent 监控数据失败')
} finally {
loading.value = false
}
}
// 刷新
function refreshData() {
loadData()
}
// 监听天数变化
watch(days, () => {
loadData()
})
onMounted(() => {
loadData()
// 每30秒自动刷新
setInterval(() => {
loadData()
}, 30000)
})
</script>
<style scoped>
.agent-dashboard-page {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-header h2 {
margin: 0;
}
.header-actions {
display: flex;
align-items: center;
}
.overview-section,
.trend-section,
.section {
margin-bottom: 30px;
}
.overview-section h3,
.trend-section h3,
.section h3 {
margin-bottom: 20px;
color: #409eff;
}
.stat-card {
text-align: center;
padding: 20px;
}
.stat-value {
font-size: 28px;
font-weight: bold;
margin-bottom: 10px;
}
.stat-label {
font-size: 14px;
color: #909399;
}
.trend-chart {
display: flex;
align-items: flex-end;
justify-content: space-between;
height: 200px;
padding: 10px;
border-bottom: 1px solid #ebeef5;
}
.trend-bar {
flex: 1;
margin: 0 2px;
background: linear-gradient(to top, #409eff, #79bbff);
border-radius: 4px 4px 0 0;
position: relative;
min-height: 20px;
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 5px;
cursor: pointer;
transition: opacity 0.3s;
}
.trend-bar:hover {
opacity: 0.8;
}
.trend-value {
font-size: 10px;
color: white;
font-weight: bold;
}
.trend-labels {
display: flex;
justify-content: space-between;
padding: 10px;
font-size: 12px;
color: #909399;
}
.trend-label {
flex: 1;
text-align: center;
}
.token-summary {
padding: 10px 0;
}
.token-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
}
.token-item .label {
font-size: 14px;
color: #909399;
}
.token-item .value {
font-size: 18px;
font-weight: bold;
color: #303133;
}
.token-item .value.total {
color: #409eff;
}
</style>

View File

@@ -118,6 +118,10 @@
<el-icon><Setting /></el-icon>
设计
</el-button>
<el-button link type="primary" @click="handleConfig(row)">
<el-icon><Operation /></el-icon>
配置
</el-button>
<el-button link type="info" @click="handleDuplicate(row)">
<el-icon><CopyDocument /></el-icon>
复制
@@ -394,7 +398,8 @@ import {
Download,
UploadFilled,
ChatDotRound,
Tools
Tools,
Operation
} from '@element-plus/icons-vue'
import { useAgentStore } from '@/stores/agent'
import type { Agent } from '@/stores/agent'
@@ -722,6 +727,14 @@ const handleDesign = (agent: Agent) => {
})
}
// 配置页
const handleConfig = (agent: Agent) => {
router.push({
name: 'agent-config',
params: { id: agent.id }
})
}
// 部署
const handleDeploy = async (agent: Agent) => {
try {