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 { fetchAgentRuns } from '../../services/agentAssets.js' import { fetchSystemLogEntries } from '../../services/systemLogs.js' import { useSystemState } from '../../composables/useSystemState.js' import { useToast } from '../../composables/useToast.js' import { isManagerUser } from '../../utils/accessControl.js' const POLL_INTERVAL_MS = 5000 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(status) { if (status === 'running') { return '运行中' } if (status === 'succeeded') { return '已完成' } if (status === 'failed') { return '失败' } if (status === 'blocked') { return '待确认' } return status || '未知' } function resolveStatusTone(status) { if (status === 'running') { return 'warning' } if (status === 'succeeded') { return 'success' } if (status === 'failed') { return 'danger' } if (status === 'blocked') { return 'muted' } return 'muted' } function resolveRunSourceLabel(source) { return SOURCE_LABELS[source] || source || '未标记' } function resolveRunModuleLabel(run) { const routeJson = run?.route_json || {} if (routeJson.job_type === 'llm_wiki_sync') { return '知识归纳' } 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 === 'llm_wiki_sync') { return `LLM Wiki 归纳 · ${routeJson.folder || '未指定目录'}` } return `Hermes 调用 · ${resolveRunModuleLabel(run)}` } function resolveRunLevel(run) { const progress = run?.route_json?.progress || {} if (run?.status === 'failed' || run?.error_message) { return 'ERROR' } 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 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 }, 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() }, 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, resolveRunTitle, resolveStatusLabel, resolveStatusTone, resolveSystemLevelTone, resolveSystemOutcomeTone, runningRunCount, selectRun, selectSystemLog, systemEventTypeFilter, systemEventTypeOptions, systemLevelFilter, systemLevelOptions, systemLogEntries, systemLogLoading, systemSearchKeyword, totalCount, totalPages, trendSeries, visiblePageItems, visibleHermesRuns, visibleSystemLogEntries } } }