feat: 持久化诊断历史并修复 Agent 命令执行
This commit is contained in:
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user