split event log

This commit is contained in:
Julian Freeman
2025-11-26 19:49:37 -04:00
parent 9e6ad62a91
commit 79a796f037
2 changed files with 94 additions and 49 deletions

View File

@@ -9,7 +9,6 @@ use sysinfo::{System, Disks};
use wmi::{COMLibrary, WMIConnection}; use wmi::{COMLibrary, WMIConnection};
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
// [修复] 移除了未使用的 Write
use std::io::Read; use std::io::Read;
use std::process::Command; use std::process::Command;
use tauri::Emitter; use tauri::Emitter;
@@ -232,16 +231,15 @@ where T: Deref<Target = [u8]>
}) })
} }
// [修复] BlueScreenView 外部调用分析 (适配新的命令行格式和 CSV 列) // BlueScreenView 外部调用分析
fn analyze_with_bluescreenview(dump_path: &Path) -> Result<BsodAnalysisReport, String> { fn analyze_with_bluescreenview(dump_path: &Path) -> Result<BsodAnalysisReport, String> {
let bsv_exe = "BlueScreenView.exe"; let bsv_exe = "BlueScreenView.exe";
let mut temp_csv_path = std::env::temp_dir(); let mut temp_csv_path = std::env::temp_dir();
temp_csv_path.push(format!("bsod_report_{}.csv", chrono::Utc::now().timestamp_millis())); temp_csv_path.push(format!("bsod_report_{}.csv", chrono::Utc::now().timestamp_millis()));
// [修改] 命令行格式: BlueScreenView.exe <DumpFile> /scomma <OutFile>
let status = Command::new(bsv_exe) let status = Command::new(bsv_exe)
.arg(dump_path.to_string_lossy().to_string()) // 直接传文件路径 .arg(dump_path.to_string_lossy().to_string())
.arg("/scomma") .arg("/scomma")
.arg(temp_csv_path.to_string_lossy().to_string()) .arg(temp_csv_path.to_string_lossy().to_string())
.status(); .status();
@@ -250,35 +248,26 @@ fn analyze_with_bluescreenview(dump_path: &Path) -> Result<BsodAnalysisReport, S
return Err("未找到 BlueScreenView.exe无法分析内核转储文件。请将 BlueScreenView.exe 放入程序目录。".to_string()); return Err("未找到 BlueScreenView.exe无法分析内核转储文件。请将 BlueScreenView.exe 放入程序目录。".to_string());
} }
// 读取字节并转为 String (lossy 模式,防止 GBK/ANSI 乱码导致 panic)
let content_bytes = fs::read(&temp_csv_path).map_err(|_| "BlueScreenView 分析未生成有效数据。".to_string())?; let content_bytes = fs::read(&temp_csv_path).map_err(|_| "BlueScreenView 分析未生成有效数据。".to_string())?;
let _ = fs::remove_file(temp_csv_path); let _ = fs::remove_file(temp_csv_path);
let content = String::from_utf8_lossy(&content_bytes); let content = String::from_utf8_lossy(&content_bytes);
// 使用 csv crate 解析
let mut rdr = csv::ReaderBuilder::new() let mut rdr = csv::ReaderBuilder::new()
.has_headers(false) // 命令行模式导出的 CSV 没有 Header第一行就是数据 .has_headers(false)
.from_reader(content.as_bytes()); .from_reader(content.as_bytes());
for result in rdr.records() { for result in rdr.records() {
if let Ok(record) = result { if let Ok(record) = result {
// BlueScreenView CSV 格式 (基于上传的文件):
// Index 0: Dump File (112525-7375-01.dmp)
// Index 2: Bug Check String (MEMORY_MANAGEMENT)
// Index 3: Bug Check Code (0x0000001a)
// Index 8: Caused By Driver (ntoskrnl.exe)
// Index 15: Crash Address (ntoskrnl.exe+41e230)
if record.len() > 15 { if record.len() > 15 {
// 第一行就是数据,不需要跳过 Header if &record[0] == "Dump File" { continue; }
let bug_check_string = &record[2]; let bug_check_string = &record[2];
let bug_check_code = &record[3]; let bug_check_code = &record[3];
let caused_by_driver = &record[8]; let caused_by_driver = &record[8];
let crash_addr = &record[15]; let crash_addr = if record.len() > 15 { &record[15] } else { "N/A" };
let (human, recommend) = translate_bugcheck_str(bug_check_code); let (human, recommend) = translate_bugcheck_str(bug_check_code);
// 优先使用 BlueScreenView 识别出的 Bug Check String
let final_reason = if !bug_check_string.is_empty() { bug_check_string.to_string() } else { human }; let final_reason = if !bug_check_string.is_empty() { bug_check_string.to_string() } else { human };
return Ok(BsodAnalysisReport { return Ok(BsodAnalysisReport {
@@ -375,7 +364,7 @@ async fn analyze_minidump(filepath: String) -> Result<BsodAnalysisReport, String
} }
} }
// [修复] 命令:分析二进制内容的 Minidump // 命令:分析二进制内容的 Minidump
#[tauri::command] #[tauri::command]
async fn analyze_minidump_bytes(file_content: Vec<u8>) -> Result<BsodAnalysisReport, String> { async fn analyze_minidump_bytes(file_content: Vec<u8>) -> Result<BsodAnalysisReport, String> {
if file_content.is_empty() { if file_content.is_empty() {
@@ -386,7 +375,6 @@ async fn analyze_minidump_bytes(file_content: Vec<u8>) -> Result<BsodAnalysisRep
match dump_type { match dump_type {
DumpType::Minidump => { DumpType::Minidump => {
// [修复] 修复 Error 类型转换
let native_result = Minidump::read(file_content.clone()) let native_result = Minidump::read(file_content.clone())
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
.and_then(|dump| analyze_dump_data_native(dump)); .and_then(|dump| analyze_dump_data_native(dump));
@@ -394,7 +382,6 @@ async fn analyze_minidump_bytes(file_content: Vec<u8>) -> Result<BsodAnalysisRep
if let Ok(report) = native_result { if let Ok(report) = native_result {
return Ok(report); return Ok(report);
} }
// 失败回退到 BSV
}, },
DumpType::KernelDump => { DumpType::KernelDump => {
// 直接回退 // 直接回退
@@ -402,7 +389,6 @@ async fn analyze_minidump_bytes(file_content: Vec<u8>) -> Result<BsodAnalysisRep
DumpType::Unknown => return Err("无效的文件签名。请确认这是 .dmp 文件。".to_string()) DumpType::Unknown => return Err("无效的文件签名。请确认这是 .dmp 文件。".to_string())
} }
// --- 回退流程:写入临时文件调用 BlueScreenView ---
let mut temp_dump_path = std::env::temp_dir(); let mut temp_dump_path = std::env::temp_dir();
temp_dump_path.push(format!("temp_dump_{}.dmp", chrono::Utc::now().timestamp_millis())); temp_dump_path.push(format!("temp_dump_{}.dmp", chrono::Utc::now().timestamp_millis()));
@@ -582,7 +568,7 @@ async fn run_diagnosis(window: tauri::Window) -> Result<(), String> {
analysis_hint: hint, analysis_hint: hint,
}); });
} }
if events.len() >= 10 { break; } // [修改] 移除了数量限制,让它获取所有符合条件的日志
} }
} }
} }

View File

@@ -21,6 +21,13 @@
> >
<span class="nav-icon"></span> 蓝屏分析 <span class="nav-icon"></span> 蓝屏分析
</button> </button>
<button
class="nav-item"
:class="{ active: currentTab === 'logs' }"
@click="currentTab = 'logs'"
>
<span class="nav-icon"></span> 系统日志
</button>
</nav> </nav>
<div class="sidebar-footer"> <div class="sidebar-footer">
<span class="version">Pro v1.2</span> <span class="version">Pro v1.2</span>
@@ -46,9 +53,8 @@
</div> </div>
</div> </div>
<!-- 状态提示 --> <!-- 状态提示 (移除了 scanFinished div 提示) -->
<div v-if="errorMsg" class="message-box error"> {{ errorMsg }}</div> <div v-if="errorMsg" class="message-box error"> {{ errorMsg }}</div>
<div v-if="scanFinished" class="message-box success"> 扫描完成</div>
<input type="file" ref="fileInput" @change="handleFileImport" accept=".json" style="display: none" /> <input type="file" ref="fileInput" @change="handleFileImport" accept=".json" style="display: none" />
@@ -133,24 +139,25 @@
</div> </div>
</div> </div>
<!-- 5. 日志卡片 --> <!-- [已移除] 5. 蓝屏分析卡片 -->
<div v-if="report.events" class="card slide-up" :class="{ danger: report.events.length > 0 }"> <!-- 6. 关键日志 (在概览中只显示简报或完全移除留给独立Tab这里按需保留或移除) -->
<div class="card-header"> <!-- 如果你希望概览完全不显示日志可以把下面这段也注释掉或移除 -->
<h3> 关键日志</h3> <!-- 这里我保留了一个极简的日志状态卡片点击跳转 -->
</div> <div class="card slide-up" @click="currentTab = 'logs'" style="cursor: pointer;">
<div v-if="report.events.length > 0" class="list-container"> <div class="card-header">
<div v-for="(evt, idx) in report.events" :key="idx" class="list-item warning-item"> <h3> 系统日志概况</h3>
<div class="item-header"> <span class="badge" :class="report.events && report.events.length > 0 ? 'badge-red' : 'badge-green'">
<span class="event-id">ID: {{ evt.event_id }}</span> {{ report.events && report.events.length > 0 ? `${report.events.length} 条记录` : '无异常' }}
<span class="event-source" :title="evt.source">{{ evt.source }}</span> </span>
<span class="event-time">{{ formatTime(evt.time_generated) }}</span> </div>
</div> <div class="content-box">
<p class="description highlight">{{ evt.analysis_hint }}</p> <p class="description" v-if="report.events && report.events.length > 0">
<p v-if="evt.message" class="description raw-message">{{ evt.message }}</p> 最近30天发现关键错误点击查看详情
</div> </p>
</div> <p class="description" v-else>系统运行稳定</p>
<div v-else class="good-news">无致命错误日志</div> </div>
</div> </div>
</div> </div>
</div> </div>
@@ -224,6 +231,41 @@
</div> </div>
</div> </div>
<!-- TAB 3: 系统日志 -->
<div v-if="currentTab === 'logs'" class="tab-view logs-view">
<div class="view-header">
<h2>系统关键日志 (近30天)</h2>
<div class="actions-row">
<button class="secondary-btn" @click="startScan" :disabled="loading">🔄 刷新日志</button>
</div>
</div>
<div class="logs-container fade-in">
<div v-if="!report.events" class="empty-state">
<span v-if="loading">正在读取日志记录...</span>
<span v-else>请先点击全面体检刷新日志以获取数据</span>
</div>
<div v-else-if="report.events.length === 0" class="good-news large">
过去30天内未发现 Kernel-Power(41) WHEA(18/19) 等致命错误
</div>
<div v-else class="log-list-full">
<div v-for="(evt, idx) in report.events" :key="idx" class="log-card warning-item">
<div class="log-header">
<div class="log-meta">
<span class="event-id">ID: {{ evt.event_id }}</span>
<span class="event-time">{{ formatTime(evt.time_generated) }}</span>
</div>
<span class="event-source">{{ evt.source }}</span>
</div>
<div class="log-body">
<p class="description highlight">{{ evt.analysis_hint }}</p>
<p v-if="evt.message" class="description raw-message">{{ evt.message }}</p>
</div>
</div>
</div>
</div>
</div>
</main> </main>
<!-- Toast 通知 --> <!-- Toast 通知 -->
@@ -245,14 +287,15 @@ import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event'; import { listen } from '@tauri-apps/api/event';
// --- 状态管理 --- // --- 状态管理 ---
const currentTab = ref('overview'); // 'overview' | 'bsod' const currentTab = ref('overview'); // 'overview' | 'bsod' | 'logs'
// 概览相关 // 概览相关
const emptyReport = () => ({ hardware: null, storage: null, events: null, minidumps: null, drivers: null, battery: null }); const emptyReport = () => ({ hardware: null, storage: null, events: null, minidumps: null, drivers: null, battery: null });
const report = ref(emptyReport()); const report = ref(emptyReport());
const loading = ref(false); const loading = ref(false);
const errorMsg = ref(''); const errorMsg = ref('');
const scanFinished = ref(false); // [移除] scanFinished 状态变量,因为不再需要显示 div 提示
// const scanFinished = ref(false);
const fileInput = ref(null); const fileInput = ref(null);
let unlistenFns = []; let unlistenFns = [];
@@ -270,7 +313,7 @@ let toastTimer = null;
const hasStorageDanger = computed(() => report.value?.storage?.some(d => d.is_danger) || false); const hasStorageDanger = computed(() => report.value?.storage?.some(d => d.is_danger) || false);
const isReportValid = computed(() => report.value && (report.value.hardware || report.value.storage)); const isReportValid = computed(() => report.value && (report.value.hardware || report.value.storage));
// --- 辅助函数 --- // --- 辅助函数 (保留) ---
function calculatePercent(used, total) { return (!total || total === 0) ? 0 : Math.round((used / total) * 100); } function calculatePercent(used, total) { return (!total || total === 0) ? 0 : Math.round((used / total) * 100); }
function getProgressStyle(used, total) { function getProgressStyle(used, total) {
const percent = calculatePercent(used, total); const percent = calculatePercent(used, total);
@@ -280,7 +323,7 @@ function getProgressStyle(used, total) {
function getBatteryColor(p) { return p < 50 ? '#ff4757' : (p < 80 ? '#ffa502' : '#2ed573'); } function getBatteryColor(p) { return p < 50 ? '#ff4757' : (p < 80 ? '#ffa502' : '#2ed573'); }
function formatTime(t) { return !t ? "Unknown Time" : t.replace('T', ' ').substring(0, 19); } function formatTime(t) { return !t ? "Unknown Time" : t.replace('T', ' ').substring(0, 19); }
// [新增] JS日期格式化函数yyyy-MM-dd HH:mm:ss // JS日期格式化函数yyyy-MM-dd HH:mm:ss
function formatJsDate(date) { function formatJsDate(date) {
const y = date.getFullYear(); const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0'); const m = String(date.getMonth() + 1).padStart(2, '0');
@@ -309,10 +352,13 @@ async function exportReport() {
} catch (err) { triggerToast('导出失败', err.message, 'error'); } } catch (err) { triggerToast('导出失败', err.message, 'error'); }
} }
function triggerImport() { fileInput.value.click(); } function triggerImport() { fileInput.value.click(); }
function handleFileImport(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const json = JSON.parse(e.target.result); if (json && (json.hardware || json.storage)) { report.value = json; scanFinished.value = false; triggerToast('导入成功', '已加载历史报告'); } else { triggerToast('导入失败', '文件格式错误', 'error'); } } catch (err) { triggerToast('解析失败', err.message, 'error'); } }; reader.readAsText(file); event.target.value = ''; } function handleFileImport(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const json = JSON.parse(e.target.result); if (json && (json.hardware || json.storage)) { report.value = json; triggerToast('导入成功', '已加载历史报告'); } else { triggerToast('导入失败', '文件格式错误', 'error'); } } catch (err) { triggerToast('解析失败', err.message, 'error'); } }; reader.readAsText(file); event.target.value = ''; }
async function startScan() { async function startScan() {
report.value = emptyReport(); errorMsg.value = ''; scanFinished.value = false; loading.value = true; // 只有在从未扫描过时清空,或者强制刷新
// report.value = emptyReport();
errorMsg.value = '';
loading.value = true;
if (unlistenFns.length > 0) { for (const fn of unlistenFns) fn(); unlistenFns = []; } if (unlistenFns.length > 0) { for (const fn of unlistenFns) fn(); unlistenFns = []; }
try { try {
unlistenFns.push( unlistenFns.push(
@@ -322,7 +368,7 @@ async function startScan() {
await listen('report-minidumps', (e) => report.value.minidumps = e.payload), await listen('report-minidumps', (e) => report.value.minidumps = e.payload),
await listen('report-battery', (e) => report.value.battery = e.payload), await listen('report-battery', (e) => report.value.battery = e.payload),
await listen('report-events', (e) => report.value.events = e.payload), await listen('report-events', (e) => report.value.events = e.payload),
await listen('diagnosis-finished', () => { loading.value = false; scanFinished.value = true; triggerToast('扫描完成', '体检已结束', 'success'); }) await listen('diagnosis-finished', () => { loading.value = false; triggerToast('体检完成', '所有项目检查完毕', 'success'); })
); );
await invoke('run_diagnosis'); await invoke('run_diagnosis');
} catch (e) { loading.value = false; errorMsg.value = e; } } catch (e) { loading.value = false; errorMsg.value = e; }
@@ -470,6 +516,7 @@ body { margin: 0; padding: 0; background-color: #f4f6f9; overflow: hidden; }
.badge-red { background-color: #ff4757; } .badge-green { background-color: #2ed573; } .badge-blue { background-color: #3498db; } .badge-gray { background-color: #95a5a6; } .badge-red { background-color: #ff4757; } .badge-green { background-color: #2ed573; } .badge-blue { background-color: #3498db; } .badge-gray { background-color: #95a5a6; }
.code-tag { background: #ff4757; color: white; padding: 1px 5px; border-radius: 3px; font-size: 0.75rem; font-weight: bold; } .code-tag { background: #ff4757; color: white; padding: 1px 5px; border-radius: 3px; font-size: 0.75rem; font-weight: bold; }
.good-news { color: #27ae60; font-weight: 600; background: #eafaf1; padding: 12px; border-radius: 6px; border: 1px solid #d4efdf; text-align: center; font-size: 0.9rem; } .good-news { color: #27ae60; font-weight: 600; background: #eafaf1; padding: 12px; border-radius: 6px; border: 1px solid #d4efdf; text-align: center; font-size: 0.9rem; }
.good-news.large { padding: 40px; font-size: 1.1rem; }
.tip { margin-top: 12px; font-size: 0.85rem; color: #d35400; background: #fff3e0; padding: 10px; border-radius: 6px; border: 1px solid #ffe0b2; } .tip { margin-top: 12px; font-size: 0.85rem; color: #d35400; background: #fff3e0; padding: 10px; border-radius: 6px; border: 1px solid #ffe0b2; }
.progress-wrapper { margin-bottom: 12px; } .progress-wrapper { margin-bottom: 12px; }
.progress-bar { width: 100%; height: 8px; background: #f1f3f5; border-radius: 4px; overflow: hidden; } .progress-bar { width: 100%; height: 8px; background: #f1f3f5; border-radius: 4px; overflow: hidden; }
@@ -514,4 +561,16 @@ body { margin: 0; padding: 0; background-color: #f4f6f9; overflow: hidden; }
.message-box { padding: 10px 15px; border-radius: 6px; margin-bottom: 15px; font-size: 0.9rem; font-weight: 500; } .message-box { padding: 10px 15px; border-radius: 6px; margin-bottom: 15px; font-size: 0.9rem; font-weight: 500; }
.message-box.success { background-color: #eafaf1; color: #27ae60; border: 1px solid #d4efdf; } .message-box.success { background-color: #eafaf1; color: #27ae60; border: 1px solid #d4efdf; }
.message-box.error { background-color: #ffeaea; color: #c0392b; border: 1px solid #ffcccc; } .message-box.error { background-color: #ffeaea; color: #c0392b; border: 1px solid #ffcccc; }
/* 新增日志视图样式 */
.logs-view { display: flex; flex-direction: column; height: 100%; }
.logs-container { flex: 1; overflow-y: auto; padding-right: 5px; }
.log-list-full { display: flex; flex-direction: column; gap: 15px; }
.log-card {
background: white; border-radius: 8px; padding: 15px 20px;
border: 1px solid #eaecf0; box-shadow: 0 2px 6px rgba(0,0,0,0.02);
}
.log-card.warning-item { border-left: 4px solid #fce588; }
.log-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.log-meta { display: flex; align-items: center; gap: 10px; }
</style> </style>