From 62f729d281c8404f437f34017fb1723f67c1e599 Mon Sep 17 00:00:00 2001 From: Wxw-Gu Date: Thu, 23 Jul 2026 16:09:20 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=B7=BB=E5=8A=A0=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E8=AF=8A=E6=96=AD=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/app-logger.ts | 83 +++++++++++++++++++ src/main/index.ts | 30 +++++++ src/preload/index.d.ts | 4 + src/preload/index.ts | 4 + src/renderer/src/assets/main.css | 12 +++ .../reports/ReportTaskStatusPanel.tsx | 14 +++- .../src/hooks/useGroupReportGeneration.ts | 82 ++++++++++++++++-- src/renderer/src/main.tsx | 31 +++++++ src/shared/app-log.ts | 8 ++ 9 files changed, 258 insertions(+), 10 deletions(-) create mode 100644 src/main/app-logger.ts create mode 100644 src/shared/app-log.ts diff --git a/src/main/app-logger.ts b/src/main/app-logger.ts new file mode 100644 index 0000000..b002169 --- /dev/null +++ b/src/main/app-logger.ts @@ -0,0 +1,83 @@ +import { app, shell } from 'electron' +import fs from 'fs-extra' +import path from 'path' +import type { AppLogEntry } from '../shared/app-log' + +const MAX_LOG_BYTES = 5 * 1024 * 1024 +const REDACTED_KEY = /(?:api[-_]?key|authorization|token|secret|password|database[-_]?key)/i + +const sanitize = (value: unknown, depth = 0): unknown => { + if (depth > 4) return '[depth-limited]' + if (typeof value === 'string') { + return value + .replace(/\bsk-[a-z0-9_-]{8,}\b/gi, '***') + .replace(/\bBearer\s+[a-z0-9._~-]{8,}\b/gi, 'Bearer ***') + .slice(0, 2000) + } + if (Array.isArray(value)) return value.slice(0, 30).map((item) => sanitize(item, depth + 1)) + if (value && typeof value === 'object') { + return Object.fromEntries( + Object.entries(value as Record).map(([key, item]) => [ + key, + REDACTED_KEY.test(key) ? '***' : sanitize(item, depth + 1) + ]) + ) + } + return value +} + +export class AppLogger { + private get logDir(): string { + return app.getPath('logs') + } + + get logPath(): string { + return path.join(this.logDir, 'wechatexplorer.log') + } + + private rotateIfNeeded(): void { + try { + if (!fs.existsSync(this.logPath) || fs.statSync(this.logPath).size < MAX_LOG_BYTES) return + const previous = `${this.logPath}.1` + if (fs.existsSync(previous)) fs.removeSync(previous) + fs.moveSync(this.logPath, previous) + } catch { + // Logging must never interrupt the application. + } + } + + write(entry: AppLogEntry): void { + try { + fs.ensureDirSync(this.logDir) + this.rotateIfNeeded() + const record = { + timestamp: new Date().toISOString(), + mode: app.isPackaged ? 'packaged' : 'development', + level: entry.level, + scope: String(entry.scope || 'app').slice(0, 80), + message: String(entry.message || '').slice(0, 500), + details: sanitize(entry.details || {}) + } + fs.appendFileSync(this.logPath, `${JSON.stringify(record)}\n`, { encoding: 'utf8' }) + if (!app.isPackaged) { + const method = + entry.level === 'error' + ? console.error + : entry.level === 'warn' + ? console.warn + : console.log + method(`[${record.scope}] ${record.message}`, record.details) + } + } catch { + // Logging must never interrupt the application. + } + } + + reveal(): void { + fs.ensureDirSync(this.logDir) + if (!fs.existsSync(this.logPath)) fs.writeFileSync(this.logPath, '', 'utf8') + shell.showItemInFolder(this.logPath) + } +} + +export const appLogger = new AppLogger() diff --git a/src/main/index.ts b/src/main/index.ts index a9bf5c3..9319c43 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -69,6 +69,8 @@ import { } from './services/bootstrap-cache' import { installSafeConsole } from './safe-log' import { agentHubService } from './services/agent-hub-service' +import { appLogger } from './app-logger' +import type { AppLogEntry } from '../shared/app-log' // electron-vite can close the child's stdout/stderr after spawning Electron. // Plain console.error then throws EPIPE on a closed pipe and crashes the IPC @@ -141,6 +143,31 @@ function createWindow(): void { // 鏌愪簺 API 鍙兘鍦ㄦ浜嬩欢鍙戠敓鍚庝娇鐢? app.whenReady().then(async () => { console.log(`WechatExplorer main build: ${BUILD_MARK}`) + appLogger.write({ + level: 'info', + scope: 'lifecycle', + message: 'WechatExplorer 启动', + details: { build: BUILD_MARK, platform: process.platform, version: app.getVersion() } + }) + process.on('uncaughtException', (error) => { + appLogger.write({ + level: 'error', + scope: 'main-process', + message: error.message, + details: { stack: error.stack } + }) + }) + process.on('unhandledRejection', (reason) => { + appLogger.write({ + level: 'error', + scope: 'main-process', + message: reason instanceof Error ? reason.message : 'Promise 未处理拒绝', + details: { + stack: reason instanceof Error ? reason.stack : undefined, + reason: reason instanceof Error ? undefined : String(reason) + } + }) + }) // WCDB's Windows runtime returns -1006 if wcdb_init is called more than once // per process. Bootstrap native once here so any later Wcdb4Client instance @@ -166,6 +193,9 @@ app.whenReady().then(async () => { // IPC test ipcMain.on('ping', () => console.log('pong')) + ipcMain.handle('app-log:write', (_, entry: AppLogEntry) => appLogger.write(entry)) + ipcMain.handle('app-log:getPath', () => appLogger.logPath) + ipcMain.handle('app-log:reveal', () => appLogger.reveal()) ipcMain.handle('db:init', async (_, key: string) => { if (dbInitInFlight) return dbInitInFlight diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index aea3fd3..bdc9810 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -38,6 +38,7 @@ import type { ImageInsight } from '../shared/image-insight' import type { AgentHubActionResult, AgentHubLogEntry, AgentHubStatus } from '../shared/agent-hub' +import type { AppLogEntry } from '../shared/app-log' export type ParsedContent = | { type: 'text'; content: string } @@ -71,6 +72,9 @@ declare global { interface Window { electron: ElectronAPI api: { + writeAppLog: (entry: AppLogEntry) => Promise + getAppLogPath: () => Promise + revealAppLog: () => Promise initDb: ( key: string ) => Promise diff --git a/src/preload/index.ts b/src/preload/index.ts index 0173272..f366f15 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -16,9 +16,13 @@ import type { ImageInsight } from '../shared/image-insight' import type { AgentHubLogEntry, AgentHubStatus } from '../shared/agent-hub' +import type { AppLogEntry } from '../shared/app-log' // 渲染器的自定义 API const api = { + writeAppLog: (entry: AppLogEntry) => ipcRenderer.invoke('app-log:write', entry), + getAppLogPath: () => ipcRenderer.invoke('app-log:getPath'), + revealAppLog: () => ipcRenderer.invoke('app-log:reveal'), initDb: (key: string) => ipcRenderer.invoke('db:init', key), getBootstrapCache: () => ipcRenderer.invoke('db:getBootstrapCache'), getContacts: (filter?: string) => ipcRenderer.invoke('db:getContacts', filter), diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 582c05a..5d8e5d2 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -3854,6 +3854,18 @@ body { margin: 4px 0 10px; } +.report-task-error button + button { + margin-left: 8px; +} + +.report-task-log-path { + display: block; + margin-top: 8px; + overflow-wrap: anywhere; + color: var(--wxex-text-secondary); + font: 11px/16px var(--wxex-font); +} + @media (max-width: 1120px) { .report-page { grid-template-columns: 268px minmax(360px, 1fr) 280px; diff --git a/src/renderer/src/components/reports/ReportTaskStatusPanel.tsx b/src/renderer/src/components/reports/ReportTaskStatusPanel.tsx index a33d84b..8ceb9ae 100644 --- a/src/renderer/src/components/reports/ReportTaskStatusPanel.tsx +++ b/src/renderer/src/components/reports/ReportTaskStatusPanel.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect, useState } from 'react' import { ReportGenerationPhase } from '../../hooks/useGroupReportGeneration' interface ReportTaskStatusPanelProps { @@ -27,6 +27,14 @@ export function ReportTaskStatusPanel({ }: ReportTaskStatusPanelProps): React.ReactElement { const activeIndex = phaseIndex(phase) const completedAll = phase === 'success' + const [logPath, setLogPath] = useState('') + + useEffect(() => { + void window.api + .getAppLogPath() + .then(setLogPath) + .catch(() => undefined) + }, []) return (