- 新增 documentPreviewAssets 工具,统一从 URL/Blob/File 推断预览类型(image/pdf/file/unsupported) - SettingsView/SettingsView.js/settingsModelHelper 新增系统缓存管理区块,调用 /settings/cache/clear 并展示清理结果;useSettings/services 适配 - WorkbenchAiFilePreviewDialog/useWorkbenchAiFilePreview 接入预览资产工具,workbenchAiComposerModel 调整文件处理 - ReceiptFolder/LogDetailView/DigitalEmployeeWorkRecords/travelReimbursementAttachmentModel 配套适配 - 新增 settings-cache-management-section 测试,更新 settings-llm/rendering/receipt-folder-view/composer-components/attachment-association 测试
461 lines
16 KiB
Vue
461 lines
16 KiB
Vue
<template>
|
||
<section class="log-detail-page">
|
||
<div class="detail-scroll">
|
||
<article v-if="loading" class="panel detail-state">
|
||
<i class="mdi mdi-loading mdi-spin"></i>
|
||
<strong>正在加载日志详情</strong>
|
||
<p>系统正在读取当前记录的结构化信息。</p>
|
||
</article>
|
||
|
||
<article v-else-if="error" class="panel detail-state error">
|
||
<i class="mdi mdi-alert-circle-outline"></i>
|
||
<strong>日志详情加载失败</strong>
|
||
<p>{{ error }}</p>
|
||
<button type="button" @click="loadDetail">重新加载</button>
|
||
</article>
|
||
|
||
<template v-else-if="isHermes && hermesRun">
|
||
<article class="detail-hero panel">
|
||
<div class="hero-copy">
|
||
<div class="hero-tags">
|
||
<span class="level-pill" :class="resolveLevelTone(resolveRunLevel(hermesRun))">
|
||
{{ resolveRunLevel(hermesRun) }}
|
||
</span>
|
||
<span class="status-pill" :class="resolveStatusTone(hermesRun)">
|
||
{{ resolveStatusLabel(hermesRun) }}
|
||
</span>
|
||
</div>
|
||
<h2>{{ resolveRunTitle(hermesRun) }}</h2>
|
||
<p>{{ hermesRun.result_summary || '暂无运行摘要。' }}</p>
|
||
<p v-if="hermesRun.status === 'running'" class="hero-hint">运行中每 5 秒自动刷新一次详情。</p>
|
||
</div>
|
||
<div class="hero-actions">
|
||
<button class="refresh-btn" type="button" :disabled="loading" @click="loadDetail">
|
||
<i class="mdi mdi-refresh"></i>
|
||
<span>刷新详情</span>
|
||
</button>
|
||
</div>
|
||
</article>
|
||
|
||
<article
|
||
v-if="hermesRunAlert"
|
||
class="panel detail-alert"
|
||
:class="hermesRunAlert.tone"
|
||
>
|
||
{{ hermesRunAlert.message }}
|
||
</article>
|
||
|
||
<div class="detail-grid">
|
||
<article class="panel detail-card wide">
|
||
<div class="card-head">
|
||
<h3>基本信息</h3>
|
||
<p>围绕当前 Hermes 任务查看关键字段。</p>
|
||
</div>
|
||
<div class="info-grid">
|
||
<div><span>Trace ID</span><strong>{{ hermesRun.run_id }}</strong></div>
|
||
<div><span>开始时间</span><strong>{{ formatDateTime(hermesRun.started_at) }}</strong></div>
|
||
<div><span>结束时间</span><strong>{{ formatDateTime(hermesRun.finished_at) }}</strong></div>
|
||
<div><span>来源</span><strong>{{ resolveRunSourceLabel(hermesRun.source) }}</strong></div>
|
||
<div><span>模块</span><strong>{{ resolveRunModuleLabel(hermesRun) }}</strong></div>
|
||
<div><span>当前阶段</span><strong>{{ hermesRunStatus.phaseLabel }}</strong></div>
|
||
<div><span>当前进度</span><strong>{{ resolveRunProgress(hermesRun) }}</strong></div>
|
||
<div><span>执行耗时</span><strong>{{ resolveRunElapsedLabel(hermesRun) }}</strong></div>
|
||
<div><span>最后心跳</span><strong>{{ resolveHeartbeatAtText(hermesRunHeartbeat) }}</strong></div>
|
||
<div><span>心跳状态</span><strong>{{ hermesRunHeartbeat.label }}</strong></div>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="panel detail-card">
|
||
<div class="card-head">
|
||
<h3>处理链路</h3>
|
||
<p>按工具调用顺序查看执行链。</p>
|
||
</div>
|
||
<div v-if="(hermesRun.tool_calls || []).length" class="trace-steps">
|
||
<button
|
||
v-for="(toolCall, index) in hermesRun.tool_calls || []"
|
||
:key="toolCall.id"
|
||
type="button"
|
||
class="trace-step"
|
||
:class="{ active: selectedToolCall?.id === toolCall.id }"
|
||
@click="selectedToolCallId = toolCall.id"
|
||
>
|
||
<span class="step-index">{{ index + 1 }}</span>
|
||
<div class="step-copy">
|
||
<strong>{{ toolCall.tool_name }}</strong>
|
||
<span>{{ resolveToolCallMeta(toolCall) }}</span>
|
||
</div>
|
||
<span class="status-pill" :class="resolveToolStatusTone(toolCall.status)">
|
||
{{ toolCall.status }}
|
||
</span>
|
||
</button>
|
||
</div>
|
||
<div v-else class="inline-empty">
|
||
当前暂无 ToolCall 明细。若长时间停在运行中且没有心跳,通常表示任务尚未真正进入 LightRAG 索引调用,或执行它的是旧版后端进程。
|
||
</div>
|
||
</article>
|
||
|
||
<article v-if="selectedToolCall" class="panel detail-card">
|
||
<div class="card-head">
|
||
<h3>当前 ToolCall</h3>
|
||
<p>查看当前工具调用的请求与返回。</p>
|
||
</div>
|
||
<div class="payload-grid">
|
||
<div>
|
||
<h4>请求参数</h4>
|
||
<pre class="code-block">{{ formatJson(selectedToolCall.request_json) }}</pre>
|
||
</div>
|
||
<div>
|
||
<h4>返回结果</h4>
|
||
<pre class="code-block">{{ formatJson(selectedToolCall.response_json) }}</pre>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="panel detail-card wide">
|
||
<div class="card-head">
|
||
<h3>路由上下文</h3>
|
||
<p>保留 Hermes 路由与进度原文,便于管理员核查。</p>
|
||
</div>
|
||
<pre class="code-block large">{{ formatJson(hermesRun.route_json) }}</pre>
|
||
</article>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else-if="isSystem && systemEntry">
|
||
<article class="detail-hero panel">
|
||
<div class="hero-copy">
|
||
<div class="hero-tags">
|
||
<span class="level-pill" :class="resolveSystemLevelTone(systemEntry.level)">
|
||
{{ systemEntry.level }}
|
||
</span>
|
||
<span class="status-pill" :class="resolveSystemOutcomeTone(systemEntry.outcome)">
|
||
{{ systemEntry.outcome }}
|
||
</span>
|
||
</div>
|
||
<h2>{{ systemEntry.summary || systemEntry.message }}</h2>
|
||
<p>{{ systemEntry.event_type }} · {{ systemEntry.logger || '未标记模块' }}</p>
|
||
</div>
|
||
<button class="refresh-btn" type="button" :disabled="loading" @click="loadDetail">
|
||
<i class="mdi mdi-refresh"></i>
|
||
<span>刷新详情</span>
|
||
</button>
|
||
</article>
|
||
|
||
<div class="detail-grid">
|
||
<article class="panel detail-card wide">
|
||
<div class="card-head">
|
||
<h3>解析结果</h3>
|
||
<p>按单条系统日志提取出的结构化字段。</p>
|
||
</div>
|
||
<div class="info-grid">
|
||
<div><span>发生时间</span><strong>{{ formatDateTime(systemEntry.timestamp) }}</strong></div>
|
||
<div><span>日志来源</span><strong>{{ systemEntry.source_file }} #{{ systemEntry.line_number }}</strong></div>
|
||
<div><span>模块</span><strong>{{ systemEntry.logger || '未标记' }}</strong></div>
|
||
<div><span>Request ID</span><strong>{{ systemEntry.request_id || '—' }}</strong></div>
|
||
<div><span>请求方法</span><strong>{{ systemEntry.method || '—' }}</strong></div>
|
||
<div><span>响应状态</span><strong>{{ systemEntry.status_code ?? '—' }}</strong></div>
|
||
<div><span>请求路径</span><strong>{{ systemEntry.path || '—' }}</strong></div>
|
||
<div><span>处理耗时</span><strong>{{ systemEntry.duration_ms == null ? '—' : `${systemEntry.duration_ms}ms` }}</strong></div>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="panel detail-card">
|
||
<div class="card-head">
|
||
<h3>解析反馈</h3>
|
||
<p>系统对当前日志的归类与处理建议。</p>
|
||
</div>
|
||
<div class="feedback-grid">
|
||
<div><span>事件类型</span><strong>{{ systemEntry.event_type }}</strong></div>
|
||
<div><span>解析状态</span><strong>{{ resolveSystemParseLabel(systemEntry.parse_status) }}</strong></div>
|
||
<div><span>处理结果</span><strong>{{ systemEntry.outcome }}</strong></div>
|
||
<div><span>建议动作</span><strong>{{ resolveSystemRecommendation(systemEntry) }}</strong></div>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="panel detail-card">
|
||
<div class="card-head">
|
||
<h3>原始日志</h3>
|
||
<p>保留该条记录的完整原文,便于排障核对。</p>
|
||
</div>
|
||
<pre class="code-block large">{{ systemEntry.raw }}</pre>
|
||
</article>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<footer class="detail-actions">
|
||
<button class="back-action" type="button" @click="backToLogs">
|
||
<i class="mdi mdi-arrow-left"></i>
|
||
<span>返回系统日志</span>
|
||
</button>
|
||
</footer>
|
||
</section>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
|
||
import { fetchAgentRunDetail } from '../services/agentAssets.js'
|
||
import { fetchSystemLogEntry } from '../services/systemLogs.js'
|
||
import {
|
||
AGENT_RUN_POLL_INTERVAL_MS,
|
||
formatAgentRunElapsed,
|
||
formatAgentRunProgress,
|
||
formatDurationShort,
|
||
resolveAgentRunHeartbeat,
|
||
resolveAgentRunStatus
|
||
} from '../utils/agentRunMonitor.js'
|
||
|
||
const SOURCE_LABELS = {
|
||
schedule: '定时任务',
|
||
system_event: '系统事件',
|
||
user_message: '用户触发'
|
||
}
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const loading = ref(false)
|
||
const error = ref('')
|
||
const hermesRun = ref(null)
|
||
const systemEntry = ref(null)
|
||
const selectedToolCallId = ref('')
|
||
const nowTick = ref(Date.now())
|
||
let pollTimer = 0
|
||
|
||
const isHermes = computed(() => route.params.logKind === 'hermes')
|
||
const isSystem = computed(() => route.params.logKind === 'system')
|
||
const selectedToolCall = computed(() =>
|
||
(hermesRun.value?.tool_calls || []).find((item) => item.id === selectedToolCallId.value) || null
|
||
)
|
||
const hermesRunStatus = computed(() => resolveAgentRunStatus(hermesRun.value, nowTick.value))
|
||
const hermesRunHeartbeat = computed(() => resolveAgentRunHeartbeat(hermesRun.value, nowTick.value))
|
||
const hermesRunAlert = computed(() => {
|
||
if (!hermesRun.value) {
|
||
return null
|
||
}
|
||
if (hermesRun.value.error_message) {
|
||
return {
|
||
tone: 'danger',
|
||
message: hermesRun.value.error_message
|
||
}
|
||
}
|
||
if (hermesRunStatus.value.isSuspicious) {
|
||
return {
|
||
tone: hermesRunStatus.value.tone === 'danger' ? 'danger' : 'warning',
|
||
message: hermesRunStatus.value.note || '当前任务长时间没有有效进展,建议检查后台执行器。'
|
||
}
|
||
}
|
||
return null
|
||
})
|
||
|
||
function formatDateTime(value) {
|
||
if (!value) return '未结束'
|
||
const date = new Date(value)
|
||
return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString('zh-CN', { hour12: false })
|
||
}
|
||
|
||
function formatJson(value) {
|
||
try {
|
||
return JSON.stringify(value || {}, null, 2)
|
||
} catch {
|
||
return String(value || '')
|
||
}
|
||
}
|
||
|
||
function resolveStatusLabel(run) {
|
||
return resolveAgentRunStatus(run, nowTick.value).label
|
||
}
|
||
|
||
function resolveStatusTone(run) {
|
||
return resolveAgentRunStatus(run, nowTick.value).tone
|
||
}
|
||
|
||
function resolveToolStatusTone(status) {
|
||
return status === 'succeeded' ? 'success' : status === 'failed' ? 'danger' : 'warning'
|
||
}
|
||
|
||
function resolveRunSourceLabel(source) {
|
||
return SOURCE_LABELS[source] || source || '未标记'
|
||
}
|
||
|
||
function resolveRunModuleLabel(run) {
|
||
const routeJson = run?.route_json || {}
|
||
if (routeJson.job_type === 'knowledge_index_sync' || routeJson.job_type === 'llm_wiki_sync') return '\u77e5\u8bc6\u5f52\u7eb3'
|
||
if (routeJson.selected_agent) return String(routeJson.selected_agent)
|
||
if (routeJson.folder) return String(routeJson.folder)
|
||
return resolveRunSourceLabel(run?.source)
|
||
}
|
||
|
||
function resolveRunTitle(run) {
|
||
const routeJson = run?.route_json || {}
|
||
if (routeJson.job_type === 'knowledge_index_sync' || routeJson.job_type === 'llm_wiki_sync') {
|
||
return `\u77e5\u8bc6\u5f52\u7eb3 \u00b7 ${routeJson.folder || '\u672a\u6307\u5b9a\u76ee\u5f55'}`
|
||
}
|
||
return `Hermes 调用 · ${resolveRunModuleLabel(run)}`
|
||
}
|
||
|
||
function resolveRunLevel(run) {
|
||
const progress = run?.route_json?.progress || {}
|
||
const statusInfo = resolveAgentRunStatus(run, nowTick.value)
|
||
if (run?.status === 'failed' || run?.error_message) return 'ERROR'
|
||
if (statusInfo.isSuspicious) return 'WARN'
|
||
if (run?.status === 'blocked' || Number(progress.failed_documents || 0) > 0) return 'WARN'
|
||
return 'INFO'
|
||
}
|
||
|
||
function resolveLevelTone(level) {
|
||
if (level === 'ERROR') return 'danger'
|
||
if (level === 'WARN') return 'warning'
|
||
if (level === 'INFO') return 'info'
|
||
return 'muted'
|
||
}
|
||
|
||
function resolveRunProgress(run) {
|
||
return formatAgentRunProgress(run)
|
||
}
|
||
|
||
function resolveSystemLevelTone(level) {
|
||
if (level === 'ERROR' || level === 'CRITICAL') return 'danger'
|
||
if (level === 'WARNING' || level === 'WARN') return 'warning'
|
||
if (level === 'INFO') return 'info'
|
||
return 'muted'
|
||
}
|
||
|
||
function resolveSystemOutcomeTone(outcome) {
|
||
if (outcome === '失败') return 'danger'
|
||
if (outcome === '异常' || outcome === '告警') return 'warning'
|
||
if (outcome === '成功') return 'success'
|
||
return 'muted'
|
||
}
|
||
|
||
function resolveSystemParseLabel(status) {
|
||
return status === 'parsed' ? '已结构化' : '待人工核查'
|
||
}
|
||
|
||
function resolveSystemRecommendation(entry) {
|
||
if (!entry) return '暂无'
|
||
if (entry.outcome === '失败') return '优先排查并关联上下游日志'
|
||
if (entry.outcome === '异常' || entry.outcome === '告警') return '建议继续关注同模块后续记录'
|
||
if (entry.parse_status !== 'parsed') return '建议人工核对原始日志'
|
||
return '无需额外处理'
|
||
}
|
||
|
||
function resolveRunElapsedLabel(run) {
|
||
const elapsed = formatAgentRunElapsed(run, nowTick.value)
|
||
if (elapsed === '—') {
|
||
return elapsed
|
||
}
|
||
return run?.status === 'running' ? `已运行 ${elapsed}` : elapsed
|
||
}
|
||
|
||
function resolveHeartbeatAtText(heartbeat) {
|
||
if (heartbeat?.at) {
|
||
return `${formatDateTime(heartbeat.at)} · ${heartbeat.text}`
|
||
}
|
||
return heartbeat?.text || '—'
|
||
}
|
||
|
||
function resolveToolCallMeta(toolCall) {
|
||
const toolType = String(toolCall?.tool_type || 'tool').trim()
|
||
if (String(toolCall?.status || '').trim() === 'running') {
|
||
const createdAt = new Date(toolCall?.created_at)
|
||
if (!Number.isNaN(createdAt.getTime())) {
|
||
return `${toolType} · 已运行 ${formatDurationShort(nowTick.value - createdAt.getTime())}`
|
||
}
|
||
return `${toolType} · 执行中`
|
||
}
|
||
|
||
const durationMs = Number(toolCall?.duration_ms || 0)
|
||
if (durationMs > 0) {
|
||
return `${toolType} · ${durationMs}ms`
|
||
}
|
||
return `${toolType} · 已结束`
|
||
}
|
||
|
||
function syncSelectedToolCall() {
|
||
const calls = hermesRun.value?.tool_calls || []
|
||
if (!calls.length) {
|
||
selectedToolCallId.value = ''
|
||
return
|
||
}
|
||
if (!calls.some((item) => item.id === selectedToolCallId.value)) {
|
||
selectedToolCallId.value = calls[0].id
|
||
}
|
||
}
|
||
|
||
function stopPolling() {
|
||
if (pollTimer) {
|
||
window.clearInterval(pollTimer)
|
||
pollTimer = 0
|
||
}
|
||
}
|
||
|
||
function syncPolling() {
|
||
stopPolling()
|
||
|
||
if (!isHermes.value || hermesRun.value?.status !== 'running') {
|
||
return
|
||
}
|
||
|
||
pollTimer = window.setInterval(() => {
|
||
nowTick.value = Date.now()
|
||
if (!loading.value) {
|
||
void loadDetail({ silent: true })
|
||
}
|
||
}, AGENT_RUN_POLL_INTERVAL_MS)
|
||
}
|
||
|
||
async function loadDetail(options = {}) {
|
||
const silent = options.silent === true
|
||
if (!silent) {
|
||
loading.value = true
|
||
}
|
||
error.value = ''
|
||
try {
|
||
const id = String(route.params.logId || '')
|
||
if (isHermes.value) {
|
||
hermesRun.value = await fetchAgentRunDetail(id)
|
||
systemEntry.value = null
|
||
syncSelectedToolCall()
|
||
return
|
||
}
|
||
if (isSystem.value) {
|
||
systemEntry.value = await fetchSystemLogEntry(id)
|
||
hermesRun.value = null
|
||
return
|
||
}
|
||
throw new Error('不支持的日志类型。')
|
||
} catch (nextError) {
|
||
error.value = nextError?.message || '日志详情加载失败。'
|
||
} finally {
|
||
nowTick.value = Date.now()
|
||
syncPolling()
|
||
if (!silent) {
|
||
loading.value = false
|
||
}
|
||
}
|
||
}
|
||
|
||
function backToLogs() {
|
||
router.push({ name: 'app-settings', query: { section: 'systemLogs' } })
|
||
}
|
||
|
||
watch(
|
||
() => [route.params.logKind, route.params.logId],
|
||
() => {
|
||
void loadDetail()
|
||
}
|
||
)
|
||
|
||
onMounted(() => {
|
||
void loadDetail()
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
stopPolling()
|
||
})
|
||
</script>
|
||
|
||
<style scoped src="../assets/styles/views/log-detail-view.css"></style>
|