feat: 增加 AI Agent 自主诊断执行模式

This commit is contained in:
cnbugs
2026-07-25 12:21:54 +08:00
parent 26ae846501
commit 4f3a7833a0
8 changed files with 659 additions and 1 deletions
+171 -1
View File
@@ -14,9 +14,16 @@
<el-button type="primary" :loading="diagnosing" @click="runDiagnosis" :icon="Refresh" :disabled="diagnosing">
{{ diagnosing ? '诊断中...' : '一键诊断' }}
</el-button>
<el-button type="success" :loading="analyzing" @click="runAnalysis" :icon="MagicStick" :disabled="!diagnosis || diagnosing">
<el-button type="success" :loading="analyzing" @click="runAnalysis" :icon="MagicStick" :disabled="!diagnosis || diagnosing || agentRunning">
{{ analyzing ? 'AI 分析中...' : 'AI 分析' }}
</el-button>
<el-radio-group v-model="agentExecutionMode" :disabled="agentRunning" class="agent-mode-switch">
<el-radio-button value="manual">手动执行命令</el-radio-button>
<el-radio-button value="ai">AI 执行命令</el-radio-button>
</el-radio-group>
<el-button type="warning" :loading="agentRunning" @click="runAgentDiagnosis" :disabled="!diagnosis || diagnosing || analyzing">
{{ agentRunning ? 'Agent 诊断中...' : '自主诊断' }}
</el-button>
</div>
</el-header>
@@ -257,6 +264,62 @@
<div class="ai-content" v-html="renderedAnalysis"></div>
</el-card>
<!-- 自主诊断 Agent -->
<el-card v-if="diagnosis && !diagnosing && (agentRunning || agentEvents.length)" class="section-card agent-card" shadow="never">
<template #header>
<div class="card-header">
<span>🧠 AI Agent 自主诊断</span>
<div class="ai-header-meta">
<el-tag :type="activeAgentMode === 'ai' ? 'success' : 'info'" size="small">
{{ activeAgentMode === 'ai' ? 'AI 执行模式' : '手动执行模式' }}
</el-tag>
<el-tag v-if="activeAgentMode === 'ai'" type="warning" size="small">修改命令人工确认</el-tag>
</div>
</div>
</template>
<el-alert
:title="activeAgentMode === 'ai'
? 'AI 执行模式:Agent 自动执行只读 kubectl 命令,修改或交互命令仍需人工批准。'
: '手动执行模式:Agent 只生成安全的 kubectl 命令,不会在服务器上执行,请复制命令到终端手动运行。'"
type="info"
:closable="false"
show-icon
class="agent-alert"
/>
<div class="agent-timeline">
<div v-for="(event, index) in agentEvents" :key="index" class="agent-event" :class="'event-' + event.type">
<div class="agent-event-title">
<el-icon v-if="event.type === 'thinking'" class="is-loading" color="#409eff"><Loading /></el-icon>
<el-icon v-else-if="event.type === 'result' || event.type === 'final'" color="#67c23a"><CircleCheckFilled /></el-icon>
<el-icon v-else-if="event.type === 'approval_required'" color="#e6a23c"><WarningFilled /></el-icon>
<el-icon v-else color="#909399"><Monitor /></el-icon>
<strong>{{ agentEventTitle(event) }}</strong>
<el-tag v-if="event.mode" :type="event.mode === 'read' ? 'success' : 'warning'" size="small">
{{ event.mode === 'read' ? '只读' : '需审批' }}
</el-tag>
</div>
<p v-if="event.message" class="agent-event-message">{{ event.message }}</p>
<p v-if="event.reason" class="agent-event-message">目的{{ event.reason }}</p>
<pre v-if="event.command" class="agent-command">{{ event.command }}</pre>
<pre v-if="event.stdout || event.stderr" class="agent-output">{{ event.stdout || event.stderr }}</pre>
<div v-if="event.type === 'final'" class="ai-content" v-html="renderMd(event.content)"></div>
<div v-if="event.type === 'manual_command'" class="manual-actions">
<el-button type="primary" plain @click="copyAgentCommand(event.command)">复制命令</el-button>
<span>请在你的终端中执行Agent 不会自动运行该命令</span>
</div>
<div v-if="event.type === 'approval_required' && !event.resolved" class="approval-actions">
<el-button type="danger" @click="resolveApproval(event, true)" :loading="event.executing">确认执行</el-button>
<el-button @click="resolveApproval(event, false)" :disabled="event.executing">拒绝</el-button>
</div>
<el-alert v-if="event.approvalResult" :title="event.approvalResult" :type="event.approvalSuccess ? 'success' : 'info'" :closable="false" />
</div>
<div v-if="agentRunning" class="agent-running">
<el-icon class="is-loading" color="#409eff"><Loading /></el-icon>
<span>Agent 正在分析命令结果并规划下一步...</span>
</div>
</div>
</el-card>
<!-- AI 对话 (连接成功或失败都显示) -->
<el-card v-if="diagnosis && !diagnosing" class="section-card" shadow="never">
<template #header><span>💬 追问 AI 助手</span></template>
@@ -288,6 +351,10 @@ const API = '/api'
const namespace = ref('')
const diagnosing = ref(false)
const analyzing = ref(false)
const agentRunning = ref(false)
const agentExecutionMode = ref('manual')
const activeAgentMode = ref('manual')
const agentEvents = ref([])
const diagnosis = ref(null)
const aiAnalysis = ref('')
const aiModel = ref('')
@@ -462,6 +529,89 @@ async function runAnalysis() {
}
}
async function runAgentDiagnosis() {
activeAgentMode.value = agentExecutionMode.value
agentRunning.value = true
agentEvents.value = []
try {
const params = new URLSearchParams()
params.set('execution_mode', activeAgentMode.value)
if (namespace.value) params.set('question', `重点检查命名空间 ${namespace.value}`)
const resp = await fetch(`${API}/agent/diagnose/stream?${params.toString()}`)
if (!resp.ok || !resp.body) throw new Error(`HTTP ${resp.status}`)
const reader = resp.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
if (!line.startsWith('data: ')) continue
try {
const event = JSON.parse(line.slice(6))
if (!['start', 'done'].includes(event.type)) agentEvents.value.push(event)
if (event.type === 'error') ElMessage.error(event.message)
} catch (e) {
// ignore partial events
}
}
}
} catch (e) {
ElMessage.error('自主诊断失败: ' + e.message)
} finally {
agentRunning.value = false
}
}
function agentEventTitle(event) {
const titles = {
thinking: 'Agent 思考', command: '准备执行命令', result: '命令执行结果',
manual_command: '请手动执行命令',
approval_required: '发现修改命令,等待审批', command_rejected: '命令已被安全策略拒绝',
final: '自主诊断结论', error: 'Agent 执行异常',
}
return titles[event.type] || event.type
}
async function copyAgentCommand(command) {
try {
await navigator.clipboard.writeText(command)
ElMessage.success('命令已复制,请在终端中手动执行')
} catch (e) {
ElMessage.error('复制失败,请手动选择命令文本')
}
}
async function resolveApproval(event, approved) {
event.executing = true
try {
const resp = await fetch(`${API}/agent/approve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ approval_id: event.approval_id, approved }),
})
const result = await resp.json()
if (!resp.ok) throw new Error(result.detail || `HTTP ${resp.status}`)
event.resolved = true
event.approvalSuccess = approved && result.returncode === 0
if (!approved) {
event.approvalResult = '已拒绝执行该命令'
} else {
event.approvalResult = result.returncode === 0 ? '命令执行成功' : `命令执行失败:${result.stderr || result.returncode}`
agentEvents.value.push({ type: 'result', ...result })
}
ElMessage[approved && result.returncode === 0 ? 'success' : 'info'](event.approvalResult)
} catch (e) {
ElMessage.error('审批处理失败: ' + e.message)
} finally {
event.executing = false
}
}
async function sendChat() {
const q = chatInput.value.trim()
if (!q || chatLoading.value) return
@@ -611,6 +761,26 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC'
.ai-content table { border-collapse: collapse; width: 100%; margin: 12px 0; }
.ai-content th, .ai-content td { border: 1px solid #ebeef5; padding: 8px 12px; text-align: left; }
.agent-card { border: 1px solid #f3d19e; }
.agent-mode-switch { flex-shrink: 0; }
.agent-alert { margin-bottom: 16px; }
.agent-timeline { display: flex; flex-direction: column; gap: 12px; }
.agent-event { padding: 14px; border: 1px solid #ebeef5; border-radius: 8px; background: #fafafa; }
.agent-event.event-thinking { border-color: #a0cfff; background: #ecf5ff; }
.agent-event.event-approval_required { border-color: #eebe77; background: #fdf6ec; }
.agent-event.event-final { border-color: #b3e19d; background: #f0f9eb; }
.agent-event-title { display: flex; align-items: center; gap: 8px; color: #303133; }
.agent-event-message { margin-top: 8px; color: #606266; font-size: 13px; }
.agent-command, .agent-output {
margin-top: 10px; padding: 12px; border-radius: 6px; overflow-x: auto;
white-space: pre-wrap; word-break: break-all; font-size: 13px;
}
.agent-command { color: #d4d4d4; background: #1e1e1e; }
.agent-output { max-height: 320px; color: #303133; background: #f4f4f5; }
.approval-actions { display: flex; gap: 8px; margin-top: 12px; }
.manual-actions { display: flex; align-items: center; gap: 12px; margin-top: 12px; color: #606266; font-size: 13px; }
.agent-running { display: flex; align-items: center; gap: 8px; color: #409eff; padding: 8px; }
.chat-box { max-height: 400px; overflow-y: auto; padding: 12px 0; display: flex; flex-direction: column; gap: 12px; }
.chat-msg { display: flex; }
.chat-msg.user { justify-content: flex-end; }