import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { useRouter } from 'vue-router' import LogTrendChart from '../../components/charts/LogTrendChart.vue' import DonutChart from '../../components/charts/DonutChart.vue' import TableLoadingState from '../../components/shared/TableLoadingState.vue' import { fetchAgentRuns } from '../../services/agentAssets.js' import { fetchSystemLogEntries } from '../../services/systemLogs.js' import { useSystemState } from '../../composables/useSystemState.js' import { useToast } from '../../composables/useToast.js' import { AGENT_RUN_POLL_INTERVAL_MS, formatAgentRunElapsed, formatAgentRunProgress, resolveAgentRunHeartbeat, resolveAgentRunStatus } from '../../utils/agentRunMonitor.js' import { isManagerUser } from '../../utils/accessControl.js' const SOURCE_LABELS = { schedule: '定时任务', system_event: '系统事件', user_message: '用户触发' } function formatDateTime(value) { if (!value) { return '未结束' } const date = new Date(value) if (Number.isNaN(date.getTime())) { return String(value) } return date.toLocaleString('zh-CN', { hour12: false }) } function resolveStatusLabel(run) { return resolveAgentRunStatus(run).label } function resolveStatusTone(run) { return resolveAgentRunStatus(run).tone } 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) 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' } if (run?.status === 'running') { return 'INFO' } return 'INFO' } function resolveLevelTone(level) { if (level === 'ERROR') { return 'danger' } if (level === 'WARN') { return 'warning' } if (level === 'INFO') { return 'info' } return 'muted' } function formatSummary(summary) { const text = String(summary || '').trim() if (!text) { return '暂无摘要。' } if (text.length <= 64) { return text } return `${text.slice(0, 64)}...` } function resolveRunSummaryMeta(run) { const statusInfo = resolveAgentRunStatus(run) const progressText = formatAgentRunProgress(run) const elapsedLabel = run?.status === 'running' ? '已运行' : '耗时' const elapsedText = formatAgentRunElapsed(run) const parts = [`阶段 ${statusInfo.phaseLabel}`] if (progressText) { parts.push(progressText) } if (elapsedText !== '—') { parts.push(`${elapsedLabel} ${elapsedText}`) } return parts.join(' · ') } function resolveRunStatusNote(run) { const statusInfo = resolveAgentRunStatus(run) if (statusInfo.note) { return statusInfo.note } const heartbeat = resolveAgentRunHeartbeat(run) if (heartbeat.at !== null) { return `最后心跳 ${formatDateTime(heartbeat.at)}` } return '暂无额外状态' } 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 formatHourBucketLabel(date) { return `${String(date.getHours()).padStart(2, '0')}:00` } function buildTrendSeries(runs) { const parsedTimes = runs .map((run) => new Date(run?.started_at)) .filter((date) => !Number.isNaN(date.getTime())) const latest = parsedTimes.length ? new Date(Math.max(...parsedTimes.map((date) => date.getTime()))) : new Date() latest.setMinutes(0, 0, 0) const buckets = Array.from({ length: 8 }, (_, index) => { const date = new Date(latest) date.setHours(latest.getHours() - (7 - index)) return { key: date.toISOString(), label: formatHourBucketLabel(date), total: 0, failed: 0 } }) const bucketMap = new Map(buckets.map((bucket) => [bucket.key, bucket])) for (const run of runs) { const date = new Date(run?.started_at) if (Number.isNaN(date.getTime())) { continue } date.setMinutes(0, 0, 0) const bucket = bucketMap.get(date.toISOString()) if (!bucket) { continue } bucket.total += 1 if (run.status === 'failed') { bucket.failed += 1 } } return { buckets, labels: buckets.map((bucket) => bucket.label), totals: buckets.map((bucket) => bucket.total), failures: buckets.map((bucket) => bucket.failed) } } export default { name: 'LogsView', components: { LogTrendChart, DonutChart, TableLoadingState }, emits: ['summary-change'], setup(_, { emit }) { const router = useRouter() const { currentUser } = useSystemState() const { toast } = useToast() const activeTab = ref('hermes') const hermesLoading = ref(false) const systemLogLoading = ref(false) const hermesRuns = ref([]) const systemSearchKeyword = ref('') const systemLevelFilter = ref('') const systemEventTypeFilter = ref('') const systemLogEntries = ref([]) const currentPage = ref(1) const pageSize = ref(10) const pageSizes = [10, 20, 50] const pageSizeOpen = ref(false) let pollTimer = 0 const isAdmin = computed(() => isManagerUser(currentUser.value)) const filteredHermesRuns = computed(() => hermesRuns.value) const systemLevelOptions = computed(() => Array.from(new Set(systemLogEntries.value.map((entry) => entry.level).filter(Boolean))) ) const systemEventTypeOptions = computed(() => Array.from(new Set(systemLogEntries.value.map((entry) => entry.event_type).filter(Boolean))) ) const filteredSystemLogEntries = computed(() => { const keyword = systemSearchKeyword.value.trim().toLowerCase() return systemLogEntries.value.filter((entry) => { if (systemLevelFilter.value && entry.level !== systemLevelFilter.value) { return false } if (systemEventTypeFilter.value && entry.event_type !== systemEventTypeFilter.value) { return false } if (!keyword) { return true } const haystack = [ entry.summary, entry.message, entry.logger, entry.request_id, entry.path, entry.event_type, entry.outcome, entry.source_file ] .filter(Boolean) .join(' ') .toLowerCase() return haystack.includes(keyword) }) }) const hermesRunCount = computed(() => hermesRuns.value.length) const runningRunCount = computed(() => hermesRuns.value.filter((run) => run.status === 'running').length) const completedRunCount = computed(() => hermesRuns.value.filter((run) => run.status === 'succeeded').length) const failedRunCount = computed(() => hermesRuns.value.filter((run) => run.status === 'failed').length) const trendSeries = computed(() => buildTrendSeries(filteredHermesRuns.value)) const levelDistribution = computed(() => { const items = [ { level: 'INFO', count: 0, color: '#3b82f6' }, { level: 'WARN', count: 0, color: '#f59e0b' }, { level: 'ERROR', count: 0, color: '#ef4444' } ] for (const run of filteredHermesRuns.value) { const item = items.find((candidate) => candidate.level === resolveRunLevel(run)) if (item) { item.count += 1 } } const total = items.reduce((sum, item) => sum + item.count, 0) return { items: items.map((item) => ({ name: item.level, value: item.count, color: item.color, display: total ? `${item.count} (${Math.round((item.count / total) * 100)}%)` : '0' })), total } }) const activeRows = computed(() => activeTab.value === 'hermes' ? filteredHermesRuns.value : filteredSystemLogEntries.value ) const totalCount = computed(() => activeRows.value.length) const totalPages = computed(() => Math.max(1, Math.ceil(totalCount.value / pageSize.value))) const visiblePageItems = computed(() => { if (totalPages.value <= 6) { return Array.from({ length: totalPages.value }, (_, index) => index + 1) } return [1, 2, 3, 4, 5, 'ellipsis', totalPages.value] }) const visibleHermesRuns = computed(() => { const start = (currentPage.value - 1) * pageSize.value return filteredHermesRuns.value.slice(start, start + pageSize.value) }) const visibleSystemLogEntries = computed(() => { const start = (currentPage.value - 1) * pageSize.value return filteredSystemLogEntries.value.slice(start, start + pageSize.value) }) function changePageSize(size) { pageSize.value = size pageSizeOpen.value = false currentPage.value = 1 } async function loadHermesRuns() { if (!isAdmin.value) { return } hermesLoading.value = true try { const payload = await fetchAgentRuns({ agent: 'hermes', limit: 100 }) hermesRuns.value = Array.isArray(payload) ? payload : [] } catch (error) { toast(error.message || 'Hermes 日志加载失败。') } finally { hermesLoading.value = false } } function selectRun(runId) { router.push({ name: 'app-log-detail', params: { logKind: 'hermes', logId: runId } }) } async function loadSystemLogs() { if (!isAdmin.value) { return } systemLogLoading.value = true try { const payload = await fetchSystemLogEntries(300) systemLogEntries.value = Array.isArray(payload) ? payload : [] } catch (error) { toast(error.message || '系统日志加载失败。') } finally { systemLogLoading.value = false } } function selectSystemLog(entryId) { router.push({ name: 'app-log-detail', params: { logKind: 'system', logId: entryId } }) } function startPolling() { stopPolling() pollTimer = window.setInterval(() => { loadHermesRuns() loadSystemLogs() }, AGENT_RUN_POLL_INTERVAL_MS) } function stopPolling() { if (pollTimer) { window.clearInterval(pollTimer) pollTimer = 0 } } watch( [ activeTab, systemSearchKeyword, systemLevelFilter, systemEventTypeFilter ], () => { currentPage.value = 1 } ) watch(totalPages, (value) => { if (currentPage.value > value) { currentPage.value = value } }) watch( () => [ hermesRunCount.value, runningRunCount.value, completedRunCount.value, failedRunCount.value ], ([total, running, completed, failed]) => { emit('summary-change', { total, running, completed, failed }) }, { immediate: true } ) onMounted(async () => { await loadHermesRuns() await loadSystemLogs() startPolling() }) onBeforeUnmount(() => { stopPolling() }) return { activeTab, changePageSize, completedRunCount, failedRunCount, filteredHermesRuns, filteredSystemLogEntries, formatDateTime, formatSummary, hermesLoading, hermesRunCount, hermesRuns, isAdmin, levelDistribution, loadHermesRuns, loadSystemLogs, currentPage, pageSize, pageSizeOpen, pageSizes, resolveLevelTone, resolveRunLevel, resolveRunModuleLabel, resolveRunSourceLabel, resolveRunStatusNote, resolveRunSummaryMeta, resolveRunTitle, resolveStatusLabel, resolveStatusTone, resolveSystemLevelTone, resolveSystemOutcomeTone, runningRunCount, selectRun, selectSystemLog, systemEventTypeFilter, systemEventTypeOptions, systemLevelFilter, systemLevelOptions, systemLogEntries, systemLogLoading, systemSearchKeyword, totalCount, totalPages, trendSeries, visiblePageItems, visibleHermesRuns, visibleSystemLogEntries } } }