feat: 增加 AI Agent 自主诊断执行模式
This commit is contained in:
+171
-1
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user