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:
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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, '<')
|
||||
.replace(/>/g, '>')
|
||||
// 代码块
|
||||
return text.replace(/</g, '<').replace(/>/g, '>')
|
||||
.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>
|
||||
|
||||
306
frontend/src/views/AgentConfig.vue
Normal file
306
frontend/src/views/AgentConfig.vue
Normal 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>
|
||||
441
frontend/src/views/AgentDashboard.vue
Normal file
441
frontend/src/views/AgentDashboard.vue
Normal 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>
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user