feat: 持久化诊断历史并修复 Agent 命令执行

This commit is contained in:
cnbugs
2026-07-25 12:40:53 +08:00
parent 4f3a7833a0
commit fdac1eaffc
10 changed files with 650 additions and 45 deletions
+126
View File
@@ -21,6 +21,7 @@
<el-radio-button value="manual">手动执行命令</el-radio-button>
<el-radio-button value="ai">AI 执行命令</el-radio-button>
</el-radio-group>
<el-button @click="openHistory" :disabled="diagnosing || analyzing || agentRunning">历史记录</el-button>
<el-button type="warning" :loading="agentRunning" @click="runAgentDiagnosis" :disabled="!diagnosis || diagnosing || analyzing">
{{ agentRunning ? 'Agent 诊断中...' : '自主诊断' }}
</el-button>
@@ -337,6 +338,61 @@
</div>
</el-card>
</el-main>
<el-drawer v-model="historyVisible" title="历史诊断与处理记录" size="75%" @open="loadHistory">
<div class="history-toolbar">
<el-select v-model="historyStatus" placeholder="全部状态" clearable style="width: 150px" @change="loadHistory">
<el-option label="健康" value="healthy" />
<el-option label="警告" value="warning" />
<el-option label="严重" value="critical" />
</el-select>
<el-input v-model="historyNamespace" placeholder="命名空间" clearable style="width: 180px" @keyup.enter="loadHistory" />
<el-button type="primary" @click="loadHistory" :loading="historyLoading">查询</el-button>
</div>
<el-table :data="historyItems" stripe highlight-current-row @row-click="loadHistoryDetail" v-loading="historyLoading">
<el-table-column prop="created_at" label="诊断时间" min-width="180" />
<el-table-column prop="namespace" label="命名空间" min-width="110" />
<el-table-column prop="score" label="评分" width="80" />
<el-table-column label="状态" width="90">
<template #default="{ row }"><el-tag :type="historyStatusType(row.status)">{{ historyStatusText(row.status) }}</el-tag></template>
</el-table-column>
<el-table-column prop="critical_count" label="严重" width="75" />
<el-table-column prop="warning_count" label="警告" width="75" />
<el-table-column prop="run_count" label="处理记录" width="100" />
<el-table-column label="操作" width="100">
<template #default="{ row }"><el-button link type="primary" @click.stop="loadHistoryDetail(row)">查看详情</el-button></template>
</el-table-column>
</el-table>
<div v-if="historyDetail" class="history-detail">
<el-divider content-position="left">诊断详情</el-divider>
<el-descriptions :column="4" border>
<el-descriptions-item label="时间">{{ historyDetail.created_at }}</el-descriptions-item>
<el-descriptions-item label="命名空间">{{ historyDetail.diagnosis.namespace }}</el-descriptions-item>
<el-descriptions-item label="健康评分">{{ historyDetail.diagnosis.score }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ historyStatusText(historyDetail.diagnosis.status) }}</el-descriptions-item>
</el-descriptions>
<el-collapse class="history-collapse">
<el-collapse-item title="完整诊断报告" name="report">
<pre class="history-report">{{ historyDetail.report }}</pre>
</el-collapse-item>
<el-collapse-item v-for="run in historyDetail.runs" :key="run.id" :name="run.id">
<template #title>
<span>{{ runKindText(run.kind) }} · {{ run.started_at }} · {{ runStatusText(run.status) }}</span>
</template>
<el-descriptions :column="3" size="small" border>
<el-descriptions-item label="模式">{{ run.mode || '-' }}</el-descriptions-item>
<el-descriptions-item label="问题">{{ run.question || '-' }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ run.finished_at || '-' }}</el-descriptions-item>
</el-descriptions>
<div v-for="(event, index) in run.events" :key="index" class="history-event">
<div class="history-event-head"><el-tag size="small">{{ event.type }}</el-tag><span>{{ event.created_at }}</span></div>
<pre>{{ formatHistoryEvent(event) }}</pre>
</div>
</el-collapse-item>
</el-collapse>
</div>
</el-drawer>
</div>
</template>
@@ -359,6 +415,12 @@ const diagnosis = ref(null)
const aiAnalysis = ref('')
const aiModel = ref('')
const analysisProcess = ref([])
const historyVisible = ref(false)
const historyLoading = ref(false)
const historyItems = ref([])
const historyDetail = ref(null)
const historyStatus = ref('')
const historyNamespace = ref('')
const chatMessages = ref([])
const chatInput = ref('')
const chatLoading = ref(false)
@@ -529,6 +591,62 @@ async function runAnalysis() {
}
}
function openHistory() {
historyVisible.value = true
}
async function loadHistory() {
historyLoading.value = true
historyDetail.value = null
try {
const params = new URLSearchParams({ limit: '200' })
if (historyStatus.value) params.set('status', historyStatus.value)
if (historyNamespace.value) params.set('namespace', historyNamespace.value)
const resp = await fetch(`${API}/history/diagnoses?${params.toString()}`)
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
const data = await resp.json()
historyItems.value = data.items || []
} catch (e) {
ElMessage.error('加载历史记录失败: ' + e.message)
} finally {
historyLoading.value = false
}
}
async function loadHistoryDetail(row) {
historyLoading.value = true
try {
const resp = await fetch(`${API}/history/diagnoses/${row.id}`)
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
historyDetail.value = await resp.json()
} catch (e) {
ElMessage.error('加载历史详情失败: ' + e.message)
} finally {
historyLoading.value = false
}
}
function historyStatusType(status) {
return status === 'healthy' ? 'success' : status === 'warning' ? 'warning' : status === 'critical' ? 'danger' : 'info'
}
function historyStatusText(status) {
return { healthy: '健康', warning: '警告', critical: '严重' }[status] || status
}
function runKindText(kind) {
return { analysis: 'AI 分析', agent: 'Agent 自主诊断', chat: 'AI 对话' }[kind] || kind
}
function runStatusText(status) {
return { running: '运行中', completed: '已完成', waiting: '等待处理', failed: '失败', rejected: '已拒绝' }[status] || status
}
function formatHistoryEvent(event) {
const { type, created_at, ...payload } = event
return JSON.stringify(payload, null, 2)
}
async function runAgentDiagnosis() {
activeAgentMode.value = agentExecutionMode.value
agentRunning.value = true
@@ -781,6 +899,14 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC'
.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; }
.history-toolbar { display: flex; gap: 10px; margin-bottom: 16px; }
.history-detail { margin-top: 20px; }
.history-collapse { margin-top: 16px; }
.history-report { max-height: 480px; padding: 14px; overflow: auto; border-radius: 6px; background: #f4f4f5; white-space: pre-wrap; }
.history-event { margin-top: 10px; padding: 12px; border: 1px solid #ebeef5; border-radius: 6px; }
.history-event-head { display: flex; align-items: center; gap: 10px; color: #909399; font-size: 12px; }
.history-event pre { max-height: 300px; margin-top: 8px; padding: 10px; overflow: auto; background: #f4f4f5; white-space: pre-wrap; }
.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; }