From cb42d4b662d704e15f170376e362afb5306f410f Mon Sep 17 00:00:00 2001 From: liangxu Date: Fri, 8 May 2026 22:12:28 +0800 Subject: [PATCH] fix(desktop): keep oauth popups alive on redirect aborts and blank loads Toutiao's wap_login redirect chain raises ERR_ABORTED (-3) without the literal symbol in the message, so the previous regex fell through to the fallback error page. Now match the numeric form too, and add a 7s blank- page watchdog that auto-reloads stuck "Loading..." popups once per fresh navigation. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/main/external-window.ts | 101 +++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/apps/desktop-client/src/main/external-window.ts b/apps/desktop-client/src/main/external-window.ts index 90f61fa..85ba539 100644 --- a/apps/desktop-client/src/main/external-window.ts +++ b/apps/desktop-client/src/main/external-window.ts @@ -2,16 +2,95 @@ import type { BrowserWindow } from 'electron/main' import { STANDARD_USER_AGENT } from './user-agent' -const ignoredNavigationFailurePattern = /ERR_ABORTED|ERR_BLOCKED_BY_CLIENT/i +const ignoredNavigationFailurePattern = /ERR_ABORTED|ERR_BLOCKED_BY_CLIENT|\((-3|-20)\) loading/i const errorPagePrefix = 'data:text/html' +const BLANK_PAGE_PROBE_DELAY_MS = 7_000 +const BLANK_PAGE_NODE_THRESHOLD = 3 +const BLANK_PAGE_TEXT_THRESHOLD = 5 + interface WindowNavigationState { readyForDetection: boolean lastMainFrameError: string | null + blankPageWatchdog: NodeJS.Timeout | null + blankPageRetried: boolean + context: string } const navigationStateRegistry = new WeakMap() +function clearBlankPageWatchdog(state: WindowNavigationState): void { + if (state.blankPageWatchdog) { + clearTimeout(state.blankPageWatchdog) + state.blankPageWatchdog = null + } +} + +async function probePageBodyShape( + window: BrowserWindow, +): Promise<{ nodeCount: number; textLength: number; readyState: string } | null> { + if (window.isDestroyed()) { + return null + } + try { + return (await window.webContents.executeJavaScript( + `(() => ({ + nodeCount: document.body ? document.body.querySelectorAll('*').length : 0, + textLength: document.body ? (document.body.innerText || '').trim().length : 0, + readyState: document.readyState, + }))()`, + true, + )) as { nodeCount: number; textLength: number; readyState: string } + } catch { + return null + } +} + +function scheduleBlankPageWatchdog(window: BrowserWindow, state: WindowNavigationState): void { + clearBlankPageWatchdog(state) + + state.blankPageWatchdog = setTimeout(() => { + state.blankPageWatchdog = null + if (window.isDestroyed()) { + return + } + + const currentURL = window.webContents.getURL() + if (currentURL.startsWith(errorPagePrefix) || currentURL === 'about:blank') { + return + } + + void probePageBodyShape(window).then((shape) => { + if (!shape || window.isDestroyed()) { + return + } + + const looksBlank = + shape.nodeCount < BLANK_PAGE_NODE_THRESHOLD && + shape.textLength < BLANK_PAGE_TEXT_THRESHOLD + + if (!looksBlank) { + return + } + + if (state.blankPageRetried) { + console.warn(`[desktop-window] ${state.context} still blank after auto-reload`, { + url: currentURL, + ...shape, + }) + return + } + + state.blankPageRetried = true + console.warn(`[desktop-window] ${state.context} blank page detected, reloading once`, { + url: currentURL, + ...shape, + }) + window.webContents.reloadIgnoringCache() + }) + }, BLANK_PAGE_PROBE_DELAY_MS) +} + function escapeHtml(value: string): string { return value .replaceAll('&', '&') @@ -126,6 +205,9 @@ export function attachWindowDiagnostics(window: BrowserWindow, context: string): navigationStateRegistry.set(window, { readyForDetection: false, lastMainFrameError: null, + blankPageWatchdog: null, + blankPageRetried: false, + context, }) window.webContents.on('did-start-loading', () => { @@ -134,6 +216,7 @@ export function attachWindowDiagnostics(window: BrowserWindow, context: string): return } state.readyForDetection = false + clearBlankPageWatchdog(state) }) window.webContents.on( @@ -147,6 +230,7 @@ export function attachWindowDiagnostics(window: BrowserWindow, context: string): if (state) { state.readyForDetection = false state.lastMainFrameError = `${errorCode}:${errorDescription}` + clearBlankPageWatchdog(state) } console.warn(`[desktop-window] ${context} main-frame load failed`, { @@ -167,6 +251,7 @@ export function attachWindowDiagnostics(window: BrowserWindow, context: string): state.readyForDetection = !currentURL.startsWith(errorPagePrefix) if (state.readyForDetection) { state.lastMainFrameError = null + scheduleBlankPageWatchdog(window, state) } }) @@ -174,10 +259,18 @@ export function attachWindowDiagnostics(window: BrowserWindow, context: string): const state = navigationStateRegistry.get(window) if (state) { state.readyForDetection = false + clearBlankPageWatchdog(state) } console.error(`[desktop-window] ${context} renderer gone`, details) }) + + window.once('closed', () => { + const state = navigationStateRegistry.get(window) + if (state) { + clearBlankPageWatchdog(state) + } + }) } export function isWindowReadyForDetection(window: BrowserWindow): boolean { @@ -193,6 +286,12 @@ export async function loadWindowURLSafely( targetURL: string, context: string, ): Promise { + const state = navigationStateRegistry.get(window) + if (state) { + state.blankPageRetried = false + clearBlankPageWatchdog(state) + } + try { await window.loadURL(targetURL, { userAgent: STANDARD_USER_AGENT }) } catch (error) {