diff --git a/desktop/src/main.cjs b/desktop/src/main.cjs index b01d844..f0428e8 100644 --- a/desktop/src/main.cjs +++ b/desktop/src/main.cjs @@ -1359,6 +1359,34 @@ function getRendererConsoleLogPath() { } } +function getRendererDebugLogPath() { + try { + const dir = app.getPath("userData"); + fs.mkdirSync(dir, { recursive: true }); + return path.join(dir, "renderer-debug.log"); + } catch { + return null; + } +} + +function appendRendererDebugLog(line) { + const logPath = getRendererDebugLogPath(); + if (!logPath) return; + try { + fs.appendFileSync(logPath, line, { encoding: "utf8" }); + } catch {} +} + +function stringifyDebugDetails(details) { + if (details == null) return ""; + if (typeof details === "string") return details; + try { + return JSON.stringify(details); + } catch (err) { + return `[unserializable:${err?.message || err}]`; + } +} + function setupRendererConsoleLogging(win) { if (!debugEnabled()) return; @@ -1575,6 +1603,15 @@ function registerWindowIpc() { } }); + ipcMain.on("debug:log", (event, payload) => { + const scope = String(payload?.scope || "renderer").trim() || "renderer"; + const message = String(payload?.message || "").trim() || "(empty)"; + const url = String(payload?.url || event?.sender?.getURL?.() || "").trim(); + const details = stringifyDebugDetails(payload?.details); + const suffix = details ? ` details=${details}` : ""; + appendRendererDebugLog(`[${nowIso()}] [${scope}] ${message} url=${url}${suffix}\n`); + }); + ipcMain.handle("app:setCloseBehavior", (_event, behavior) => { try { const next = setCloseBehavior(behavior); diff --git a/desktop/src/preload.cjs b/desktop/src/preload.cjs index 501d0e3..33ab114 100644 --- a/desktop/src/preload.cjs +++ b/desktop/src/preload.cjs @@ -1,5 +1,65 @@ const { contextBridge, ipcRenderer } = require("electron"); +function sendDebugLog(scope, message, details) { + try { + ipcRenderer.send("debug:log", { + scope: String(scope || "renderer"), + message: String(message || ""), + details: details == null ? {} : details, + url: typeof location !== "undefined" ? String(location.href || "") : "", + }); + } catch {} +} + +sendDebugLog("preload", "script-start", { + userAgent: typeof navigator !== "undefined" ? String(navigator.userAgent || "") : "", +}); + +if (typeof document !== "undefined") { + document.addEventListener("readystatechange", () => { + sendDebugLog("preload", "document-readystate", { + readyState: String(document.readyState || ""), + }); + }); +} + +if (typeof window !== "undefined") { + window.addEventListener("DOMContentLoaded", () => { + sendDebugLog("preload", "dom-content-loaded"); + }); + + window.addEventListener("load", () => { + sendDebugLog("preload", "window-load"); + }); + + window.addEventListener("error", (event) => { + sendDebugLog("preload", "window-error", { + message: String(event?.message || ""), + filename: String(event?.filename || ""), + lineno: Number(event?.lineno || 0), + colno: Number(event?.colno || 0), + }); + }); + + window.addEventListener("unhandledrejection", (event) => { + const reason = event?.reason; + sendDebugLog("preload", "window-unhandledrejection", { + reason: + reason instanceof Error + ? { + name: String(reason.name || "Error"), + message: String(reason.message || ""), + stack: String(reason.stack || ""), + } + : String(reason || ""), + }); + }); + + window.setTimeout(() => { + sendDebugLog("preload", "set-timeout-0"); + }, 0); +} + contextBridge.exposeInMainWorld("wechatDesktop", { // Marker used by the frontend to distinguish the Electron desktop shell from the pure web build. __brand: "WeChatDataAnalysisDesktop", @@ -8,6 +68,7 @@ contextBridge.exposeInMainWorld("wechatDesktop", { close: () => ipcRenderer.invoke("window:close"), isMaximized: () => ipcRenderer.invoke("window:isMaximized"), isDebugEnabled: () => ipcRenderer.invoke("app:isDebugEnabled"), + logDebug: (scope, message, details = {}) => sendDebugLog(scope, message, details), getAutoLaunch: () => ipcRenderer.invoke("app:getAutoLaunch"), setAutoLaunch: (enabled) => ipcRenderer.invoke("app:setAutoLaunch", !!enabled), diff --git a/frontend/composables/chat/useChatMessages.js b/frontend/composables/chat/useChatMessages.js index 7acb0fd..4d710da 100644 --- a/frontend/composables/chat/useChatMessages.js +++ b/frontend/composables/chat/useChatMessages.js @@ -36,6 +36,9 @@ export const useChatMessages = ({ const logMessagePhase = (phase, details = {}) => { if (!isDesktopRenderer()) return + try { + window.wechatDesktop?.logDebug?.('chat-messages', phase, details) + } catch {} console.info(`[chat-messages] ${phase}`, { account: String(selectedAccount.value || '').trim(), selectedUsername: String(selectedContact.value?.username || '').trim(), diff --git a/frontend/pages/chat/[[username]].vue b/frontend/pages/chat/[[username]].vue index 3188acc..83631bf 100644 --- a/frontend/pages/chat/[[username]].vue +++ b/frontend/pages/chat/[[username]].vue @@ -94,6 +94,9 @@ const shouldLogChatBootstrap = () => isDesktopShell() || desktopDebugEnabled.val const logChatBootstrap = (phase, details = {}) => { if (!shouldLogChatBootstrap()) return + try { + window.wechatDesktop?.logDebug?.('chat-bootstrap', phase, details) + } catch {} console.info(`[chat-bootstrap] ${phase}`, { elapsedMs: chatBootstrapElapsedMs(), route: route.fullPath, diff --git a/frontend/plugins/desktop-debug.client.js b/frontend/plugins/desktop-debug.client.js new file mode 100644 index 0000000..065e94c --- /dev/null +++ b/frontend/plugins/desktop-debug.client.js @@ -0,0 +1,83 @@ +const isDesktopShell = () => { + if (typeof window === 'undefined') return false + return !!window.wechatDesktop?.__brand +} + +const formatError = (error) => { + if (!error) return '' + if (error instanceof Error) { + return { + name: String(error.name || 'Error'), + message: String(error.message || ''), + stack: String(error.stack || '') + } + } + if (typeof error === 'object') { + try { + return JSON.parse(JSON.stringify(error)) + } catch {} + } + return String(error) +} + +const logDesktopDebug = (phase, details = {}) => { + if (!isDesktopShell()) return + try { + window.wechatDesktop?.logDebug?.('nuxt-bootstrap', phase, { + href: String(window.location?.href || ''), + ...details + }) + } catch {} + try { + console.info(`[nuxt-bootstrap] ${phase}`, details) + } catch {} +} + +export default defineNuxtPlugin((nuxtApp) => { + logDesktopDebug('plugin:setup') + + if (typeof window !== 'undefined') { + window.addEventListener('error', (event) => { + logDesktopDebug('window:error', { + message: String(event?.message || ''), + filename: String(event?.filename || ''), + lineno: Number(event?.lineno || 0), + colno: Number(event?.colno || 0), + error: formatError(event?.error) + }) + }) + + window.addEventListener('unhandledrejection', (event) => { + logDesktopDebug('window:unhandledrejection', { + reason: formatError(event?.reason) + }) + }) + } + + nuxtApp.hook('app:created', () => { + logDesktopDebug('app:created') + }) + + nuxtApp.hook('app:beforeMount', () => { + logDesktopDebug('app:beforeMount') + }) + + nuxtApp.hook('app:mounted', () => { + logDesktopDebug('app:mounted') + }) + + nuxtApp.hook('page:start', () => { + logDesktopDebug('page:start') + }) + + nuxtApp.hook('page:finish', () => { + logDesktopDebug('page:finish') + }) + + nuxtApp.hook('vue:error', (error, _instance, info) => { + logDesktopDebug('vue:error', { + info: String(info || ''), + error: formatError(error) + }) + }) +})