2026-05-01 20:39:09 +08:00
|
|
|
import type { BrowserWindow } from 'electron/main'
|
2026-04-19 14:18:20 +08:00
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
import { STANDARD_USER_AGENT } from './user-agent'
|
2026-04-19 14:18:20 +08:00
|
|
|
|
2026-05-08 22:12:28 +08:00
|
|
|
const ignoredNavigationFailurePattern = /ERR_ABORTED|ERR_BLOCKED_BY_CLIENT|\((-3|-20)\) loading/i
|
2026-05-01 20:39:09 +08:00
|
|
|
const errorPagePrefix = 'data:text/html'
|
2026-04-19 14:18:20 +08:00
|
|
|
|
2026-05-12 01:31:50 +08:00
|
|
|
const REMOTE_PAGE_LOAD_TIMEOUT_MS = 25_000
|
2026-05-08 22:12:28 +08:00
|
|
|
const BLANK_PAGE_PROBE_DELAY_MS = 7_000
|
|
|
|
|
const BLANK_PAGE_NODE_THRESHOLD = 3
|
|
|
|
|
const BLANK_PAGE_TEXT_THRESHOLD = 5
|
|
|
|
|
|
2026-04-19 14:18:20 +08:00
|
|
|
interface WindowNavigationState {
|
2026-05-01 20:39:09 +08:00
|
|
|
readyForDetection: boolean
|
|
|
|
|
lastMainFrameError: string | null
|
2026-05-12 01:31:50 +08:00
|
|
|
loadTimeoutWatchdog: NodeJS.Timeout | null
|
2026-05-08 22:12:28 +08:00
|
|
|
blankPageWatchdog: NodeJS.Timeout | null
|
|
|
|
|
blankPageRetried: boolean
|
2026-05-12 01:31:50 +08:00
|
|
|
localPageKind: 'loading' | 'error' | null
|
|
|
|
|
activeTargetURL: string | null
|
|
|
|
|
activeLoadToken: number
|
2026-05-08 22:12:28 +08:00
|
|
|
context: string
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
const navigationStateRegistry = new WeakMap<BrowserWindow, WindowNavigationState>()
|
2026-04-19 14:18:20 +08:00
|
|
|
|
2026-05-08 22:12:28 +08:00
|
|
|
function clearBlankPageWatchdog(state: WindowNavigationState): void {
|
|
|
|
|
if (state.blankPageWatchdog) {
|
|
|
|
|
clearTimeout(state.blankPageWatchdog)
|
|
|
|
|
state.blankPageWatchdog = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 01:31:50 +08:00
|
|
|
function clearLoadTimeoutWatchdog(state: WindowNavigationState): void {
|
|
|
|
|
if (state.loadTimeoutWatchdog) {
|
|
|
|
|
clearTimeout(state.loadTimeoutWatchdog)
|
|
|
|
|
state.loadTimeoutWatchdog = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearNavigationWatchdogs(state: WindowNavigationState): void {
|
|
|
|
|
clearLoadTimeoutWatchdog(state)
|
|
|
|
|
clearBlankPageWatchdog(state)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isLocalPageURL(url: string): boolean {
|
|
|
|
|
return url.startsWith(errorPagePrefix)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isIgnoredNavigationFailure(errorCode: number, errorDescription = ''): boolean {
|
|
|
|
|
return (
|
|
|
|
|
errorCode === -3 ||
|
|
|
|
|
errorCode === -20 ||
|
|
|
|
|
ignoredNavigationFailurePattern.test(`${errorCode}:${errorDescription}`)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-08 22:12:28 +08:00
|
|
|
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,
|
|
|
|
|
})
|
2026-05-12 01:31:50 +08:00
|
|
|
void showErrorPage(
|
|
|
|
|
window,
|
|
|
|
|
state.context,
|
|
|
|
|
currentURL,
|
|
|
|
|
'页面加载完成后仍然是空白状态,可能是网络、平台风控、兼容性或远程脚本加载失败导致。',
|
|
|
|
|
)
|
2026-05-08 22:12:28 +08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state.blankPageRetried = true
|
|
|
|
|
console.warn(`[desktop-window] ${state.context} blank page detected, reloading once`, {
|
|
|
|
|
url: currentURL,
|
|
|
|
|
...shape,
|
|
|
|
|
})
|
2026-05-12 01:31:50 +08:00
|
|
|
void navigateRemoteURL(window, currentURL, state.context, {
|
|
|
|
|
resetBlankRetry: false,
|
|
|
|
|
showLoadingPage: true,
|
|
|
|
|
})
|
2026-05-08 22:12:28 +08:00
|
|
|
})
|
|
|
|
|
}, BLANK_PAGE_PROBE_DELAY_MS)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 14:18:20 +08:00
|
|
|
function escapeHtml(value: string): string {
|
|
|
|
|
return value
|
2026-05-01 20:39:09 +08:00
|
|
|
.replaceAll('&', '&')
|
|
|
|
|
.replaceAll('<', '<')
|
|
|
|
|
.replaceAll('>', '>')
|
|
|
|
|
.replaceAll('"', '"')
|
|
|
|
|
.replaceAll("'", ''')
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-12 01:31:50 +08:00
|
|
|
function targetHostLabel(targetURL: string): string {
|
|
|
|
|
try {
|
|
|
|
|
return new URL(targetURL).host
|
|
|
|
|
} catch {
|
|
|
|
|
return targetURL
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function loadingPageDataURL(context: string, targetURL: string): string {
|
|
|
|
|
const html = `<!doctype html>
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8" />
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
|
|
|
<title>${escapeHtml(context)} - 正在打开</title>
|
|
|
|
|
<style>
|
|
|
|
|
:root {
|
|
|
|
|
color-scheme: light dark;
|
|
|
|
|
font-family: "IBM Plex Sans", "Segoe UI Variable", "Segoe UI", sans-serif;
|
|
|
|
|
background: #f6f7f9;
|
|
|
|
|
color: #111827;
|
|
|
|
|
}
|
|
|
|
|
* {
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
html {
|
|
|
|
|
width: 100%;
|
|
|
|
|
min-height: 100%;
|
|
|
|
|
}
|
|
|
|
|
body {
|
|
|
|
|
margin: 0;
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
display: grid;
|
|
|
|
|
place-items: center;
|
|
|
|
|
padding: 24px;
|
|
|
|
|
background: #f6f7f9;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
main {
|
|
|
|
|
width: min(520px, 100%);
|
|
|
|
|
padding: 28px;
|
|
|
|
|
border: 1px solid #d9dee7;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
.spinner {
|
|
|
|
|
width: 32px;
|
|
|
|
|
height: 32px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
border: 3px solid #d9dee7;
|
|
|
|
|
border-top-color: #2563eb;
|
|
|
|
|
animation: spin 0.9s linear infinite;
|
|
|
|
|
}
|
|
|
|
|
h1 {
|
|
|
|
|
margin: 18px 0 0;
|
|
|
|
|
font-size: 22px;
|
|
|
|
|
line-height: 1.35;
|
|
|
|
|
letter-spacing: 0;
|
|
|
|
|
}
|
|
|
|
|
p {
|
|
|
|
|
margin: 10px 0 0;
|
|
|
|
|
color: #5b6573;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
}
|
|
|
|
|
code {
|
|
|
|
|
display: block;
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
background: #f1f4f8;
|
|
|
|
|
color: #374151;
|
|
|
|
|
font-family: "SFMono-Regular", Consolas, monospace;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
}
|
|
|
|
|
@keyframes spin {
|
|
|
|
|
to {
|
|
|
|
|
transform: rotate(360deg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
|
|
|
:root,
|
|
|
|
|
body {
|
|
|
|
|
background: #111827;
|
|
|
|
|
color: #f9fafb;
|
|
|
|
|
}
|
|
|
|
|
main {
|
|
|
|
|
border-color: #2f3948;
|
|
|
|
|
background: #182130;
|
|
|
|
|
}
|
|
|
|
|
p {
|
|
|
|
|
color: #b6bfcb;
|
|
|
|
|
}
|
|
|
|
|
code {
|
|
|
|
|
background: #111827;
|
|
|
|
|
color: #d1d5db;
|
|
|
|
|
}
|
|
|
|
|
.spinner {
|
|
|
|
|
border-color: #344154;
|
|
|
|
|
border-top-color: #60a5fa;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<main>
|
|
|
|
|
<div class="spinner" aria-hidden="true"></div>
|
|
|
|
|
<h1>正在打开${escapeHtml(context)}</h1>
|
|
|
|
|
<p>如果平台网络较慢,窗口会停留在这里;加载失败时会给出重试入口。</p>
|
|
|
|
|
<code>${escapeHtml(targetHostLabel(targetURL))}</code>
|
|
|
|
|
</main>
|
|
|
|
|
</body>
|
|
|
|
|
</html>`
|
|
|
|
|
|
|
|
|
|
return `data:text/html;charset=UTF-8,${encodeURIComponent(html)}`
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 14:18:20 +08:00
|
|
|
function errorPageDataURL(context: string, targetURL: string, message: string): string {
|
|
|
|
|
const html = `<!doctype html>
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8" />
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
|
|
|
<title>${escapeHtml(context)}</title>
|
|
|
|
|
<style>
|
|
|
|
|
:root {
|
|
|
|
|
color-scheme: light dark;
|
|
|
|
|
font-family: "IBM Plex Sans", "Segoe UI Variable", "Segoe UI", sans-serif;
|
2026-05-12 01:31:50 +08:00
|
|
|
background: #f6f7f9;
|
|
|
|
|
color: #111827;
|
|
|
|
|
}
|
|
|
|
|
* {
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
html {
|
|
|
|
|
width: 100%;
|
|
|
|
|
min-height: 100%;
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
body {
|
|
|
|
|
margin: 0;
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
display: grid;
|
|
|
|
|
place-items: center;
|
|
|
|
|
padding: 24px;
|
2026-05-12 01:31:50 +08:00
|
|
|
background: #f6f7f9;
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
.card {
|
|
|
|
|
width: min(680px, 100%);
|
|
|
|
|
padding: 24px;
|
2026-05-12 01:31:50 +08:00
|
|
|
border-radius: 8px;
|
|
|
|
|
border: 1px solid #d9dee7;
|
|
|
|
|
background: #ffffff;
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
h1, p, pre {
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
h1 {
|
2026-05-12 01:31:50 +08:00
|
|
|
font-size: 24px;
|
|
|
|
|
letter-spacing: 0;
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
p {
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
color: #475569;
|
|
|
|
|
}
|
|
|
|
|
pre {
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
padding: 14px 16px;
|
2026-05-12 01:31:50 +08:00
|
|
|
border-radius: 6px;
|
|
|
|
|
background: #f1f4f8;
|
2026-04-19 14:18:20 +08:00
|
|
|
overflow: auto;
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
}
|
|
|
|
|
.actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
flex-wrap: wrap;
|
2026-05-12 01:31:50 +08:00
|
|
|
justify-content: center;
|
2026-04-19 14:18:20 +08:00
|
|
|
margin-top: 18px;
|
|
|
|
|
}
|
2026-05-12 01:31:50 +08:00
|
|
|
button {
|
2026-04-19 14:18:20 +08:00
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
min-height: 44px;
|
|
|
|
|
padding: 0 16px;
|
2026-05-12 01:31:50 +08:00
|
|
|
border-radius: 8px;
|
|
|
|
|
border: 1px solid #d9dee7;
|
2026-04-19 14:18:20 +08:00
|
|
|
background: white;
|
|
|
|
|
color: #0f172a;
|
|
|
|
|
font: inherit;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
2026-05-12 01:31:50 +08:00
|
|
|
button:disabled {
|
|
|
|
|
cursor: default;
|
|
|
|
|
opacity: 0.72;
|
|
|
|
|
}
|
2026-04-19 14:18:20 +08:00
|
|
|
.primary {
|
2026-05-12 01:31:50 +08:00
|
|
|
background: #2563eb;
|
|
|
|
|
border-color: #2563eb;
|
2026-04-19 14:18:20 +08:00
|
|
|
color: white;
|
|
|
|
|
}
|
2026-05-12 01:31:50 +08:00
|
|
|
@media (prefers-color-scheme: dark) {
|
|
|
|
|
:root,
|
|
|
|
|
body {
|
|
|
|
|
background: #111827;
|
|
|
|
|
color: #f9fafb;
|
|
|
|
|
}
|
|
|
|
|
.card {
|
|
|
|
|
border-color: #2f3948;
|
|
|
|
|
background: #182130;
|
|
|
|
|
}
|
|
|
|
|
p {
|
|
|
|
|
color: #b6bfcb;
|
|
|
|
|
}
|
|
|
|
|
pre {
|
|
|
|
|
background: #111827;
|
|
|
|
|
}
|
|
|
|
|
button {
|
|
|
|
|
border-color: #344154;
|
|
|
|
|
background: #111827;
|
|
|
|
|
color: #f9fafb;
|
|
|
|
|
}
|
|
|
|
|
.primary {
|
|
|
|
|
border-color: #3b82f6;
|
|
|
|
|
background: #3b82f6;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-19 14:18:20 +08:00
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<main class="card">
|
|
|
|
|
<h1>授权页加载失败</h1>
|
2026-05-12 01:31:50 +08:00
|
|
|
<p>${escapeHtml(context)} 在打开远程页面时遇到网络、TLS、平台脚本或兼容性异常。窗口已保留,可以直接重试。</p>
|
|
|
|
|
<p>如果持续失败,请检查代理、DNS、系统时间和平台是否触发风控。</p>
|
2026-04-19 14:18:20 +08:00
|
|
|
<pre>${escapeHtml(message)}
|
|
|
|
|
|
|
|
|
|
${escapeHtml(targetURL)}</pre>
|
|
|
|
|
<div class="actions">
|
2026-05-12 01:31:50 +08:00
|
|
|
<button class="primary" onclick="this.disabled = true; this.textContent = '正在重试...'; location.replace(${JSON.stringify(targetURL)})">重新加载</button>
|
2026-04-19 14:18:20 +08:00
|
|
|
</div>
|
|
|
|
|
</main>
|
|
|
|
|
</body>
|
2026-05-01 20:39:09 +08:00
|
|
|
</html>`
|
2026-04-19 14:18:20 +08:00
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
return `data:text/html;charset=UTF-8,${encodeURIComponent(html)}`
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-12 01:31:50 +08:00
|
|
|
async function showLocalPage(
|
|
|
|
|
window: BrowserWindow,
|
|
|
|
|
pageURL: string,
|
|
|
|
|
kind: WindowNavigationState['localPageKind'],
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
if (window.isDestroyed()) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const state = navigationStateRegistry.get(window)
|
|
|
|
|
if (state) {
|
|
|
|
|
state.localPageKind = kind
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await window.loadURL(pageURL)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (!window.isDestroyed()) {
|
|
|
|
|
console.warn('[desktop-window] local page load failed', {
|
|
|
|
|
message: error instanceof Error ? error.message : String(error),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function showErrorPage(
|
|
|
|
|
window: BrowserWindow,
|
|
|
|
|
context: string,
|
|
|
|
|
targetURL: string,
|
|
|
|
|
message: string,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
if (window.isDestroyed()) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const state = navigationStateRegistry.get(window)
|
|
|
|
|
if (state) {
|
|
|
|
|
state.readyForDetection = false
|
|
|
|
|
state.lastMainFrameError = message
|
|
|
|
|
clearNavigationWatchdogs(state)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await showLocalPage(window, errorPageDataURL(context, targetURL, message), 'error')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function scheduleRemoteLoadTimeout(
|
|
|
|
|
window: BrowserWindow,
|
|
|
|
|
state: WindowNavigationState,
|
|
|
|
|
targetURL: string,
|
|
|
|
|
context: string,
|
|
|
|
|
loadToken: number,
|
|
|
|
|
): void {
|
|
|
|
|
clearLoadTimeoutWatchdog(state)
|
|
|
|
|
state.loadTimeoutWatchdog = setTimeout(() => {
|
|
|
|
|
if (window.isDestroyed()) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const latestState = navigationStateRegistry.get(window)
|
|
|
|
|
if (!latestState || latestState.activeLoadToken !== loadToken) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const currentURL = window.webContents.getURL()
|
|
|
|
|
if (isLocalPageURL(currentURL) && latestState.localPageKind === 'error') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.warn(`[desktop-window] ${context} remote page load timed out`, {
|
|
|
|
|
targetURL,
|
|
|
|
|
currentURL,
|
|
|
|
|
timeoutMs: REMOTE_PAGE_LOAD_TIMEOUT_MS,
|
|
|
|
|
})
|
|
|
|
|
void showErrorPage(
|
|
|
|
|
window,
|
|
|
|
|
context,
|
|
|
|
|
targetURL,
|
|
|
|
|
`页面加载超过 ${Math.round(REMOTE_PAGE_LOAD_TIMEOUT_MS / 1000)} 秒,可能是网络连接或平台服务暂时不可用。`,
|
|
|
|
|
)
|
|
|
|
|
}, REMOTE_PAGE_LOAD_TIMEOUT_MS)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function navigateRemoteURL(
|
|
|
|
|
window: BrowserWindow,
|
|
|
|
|
targetURL: string,
|
|
|
|
|
context: string,
|
|
|
|
|
options: { resetBlankRetry: boolean; showLoadingPage: boolean },
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const state = navigationStateRegistry.get(window)
|
|
|
|
|
const loadToken = (state?.activeLoadToken ?? 0) + 1
|
|
|
|
|
|
|
|
|
|
if (state) {
|
|
|
|
|
state.activeLoadToken = loadToken
|
|
|
|
|
state.activeTargetURL = targetURL
|
|
|
|
|
state.readyForDetection = false
|
|
|
|
|
if (options.resetBlankRetry) {
|
|
|
|
|
state.blankPageRetried = false
|
|
|
|
|
}
|
|
|
|
|
clearNavigationWatchdogs(state)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (options.showLoadingPage) {
|
|
|
|
|
await showLocalPage(window, loadingPageDataURL(context, targetURL), 'loading')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (window.isDestroyed()) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (state && state.activeLoadToken === loadToken) {
|
|
|
|
|
scheduleRemoteLoadTimeout(window, state, targetURL, context, loadToken)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await window.loadURL(targetURL, { userAgent: STANDARD_USER_AGENT })
|
|
|
|
|
const latestState = navigationStateRegistry.get(window)
|
|
|
|
|
if (latestState?.activeLoadToken === loadToken) {
|
|
|
|
|
clearLoadTimeoutWatchdog(latestState)
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
|
|
|
const latestState = navigationStateRegistry.get(window)
|
|
|
|
|
if (latestState?.activeLoadToken === loadToken) {
|
|
|
|
|
clearLoadTimeoutWatchdog(latestState)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ignoredNavigationFailurePattern.test(message) || window.isDestroyed()) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.error(`[desktop-window] ${context} loadURL failed`, {
|
|
|
|
|
targetURL,
|
|
|
|
|
message,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const currentURL = window.webContents.getURL()
|
|
|
|
|
if (isLocalPageURL(currentURL) && latestState?.localPageKind === 'error') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await showErrorPage(window, context, targetURL, message)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 14:18:20 +08:00
|
|
|
export function attachWindowDiagnostics(window: BrowserWindow, context: string): void {
|
|
|
|
|
navigationStateRegistry.set(window, {
|
|
|
|
|
readyForDetection: false,
|
|
|
|
|
lastMainFrameError: null,
|
2026-05-12 01:31:50 +08:00
|
|
|
loadTimeoutWatchdog: null,
|
2026-05-08 22:12:28 +08:00
|
|
|
blankPageWatchdog: null,
|
|
|
|
|
blankPageRetried: false,
|
2026-05-12 01:31:50 +08:00
|
|
|
localPageKind: null,
|
|
|
|
|
activeTargetURL: null,
|
|
|
|
|
activeLoadToken: 0,
|
2026-05-08 22:12:28 +08:00
|
|
|
context,
|
2026-05-01 20:39:09 +08:00
|
|
|
})
|
2026-04-19 14:18:20 +08:00
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
window.webContents.on('did-start-loading', () => {
|
|
|
|
|
const state = navigationStateRegistry.get(window)
|
2026-04-19 14:18:20 +08:00
|
|
|
if (!state) {
|
2026-05-01 20:39:09 +08:00
|
|
|
return
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
state.readyForDetection = false
|
2026-05-08 22:12:28 +08:00
|
|
|
clearBlankPageWatchdog(state)
|
2026-05-01 20:39:09 +08:00
|
|
|
})
|
2026-04-19 14:18:20 +08:00
|
|
|
|
2026-05-12 01:31:50 +08:00
|
|
|
window.webContents.on(
|
|
|
|
|
'did-start-navigation',
|
|
|
|
|
(_event, url, isInPlace, isMainFrame) => {
|
|
|
|
|
if (!isMainFrame || isInPlace || !/^https?:\/\//i.test(url)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const state = navigationStateRegistry.get(window)
|
|
|
|
|
if (!state) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fromLocalPage = isLocalPageURL(window.webContents.getURL())
|
|
|
|
|
state.readyForDetection = false
|
|
|
|
|
state.activeTargetURL = url
|
|
|
|
|
if (fromLocalPage) {
|
|
|
|
|
state.localPageKind = 'loading'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fromLocalPage || !state.loadTimeoutWatchdog) {
|
|
|
|
|
state.activeLoadToken += 1
|
|
|
|
|
scheduleRemoteLoadTimeout(window, state, url, context, state.activeLoadToken)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-19 14:18:20 +08:00
|
|
|
window.webContents.on(
|
2026-05-01 20:39:09 +08:00
|
|
|
'did-fail-load',
|
2026-04-19 14:18:20 +08:00
|
|
|
(_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
2026-05-12 01:31:50 +08:00
|
|
|
if (!isMainFrame || isIgnoredNavigationFailure(errorCode, errorDescription)) {
|
2026-05-01 20:39:09 +08:00
|
|
|
return
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
const state = navigationStateRegistry.get(window)
|
2026-04-19 14:18:20 +08:00
|
|
|
if (state) {
|
2026-05-01 20:39:09 +08:00
|
|
|
state.readyForDetection = false
|
|
|
|
|
state.lastMainFrameError = `${errorCode}:${errorDescription}`
|
2026-05-12 01:31:50 +08:00
|
|
|
clearNavigationWatchdogs(state)
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.warn(`[desktop-window] ${context} main-frame load failed`, {
|
|
|
|
|
errorCode,
|
|
|
|
|
errorDescription,
|
|
|
|
|
validatedURL,
|
2026-05-01 20:39:09 +08:00
|
|
|
})
|
2026-05-12 01:31:50 +08:00
|
|
|
|
|
|
|
|
const targetURL = validatedURL || state?.activeTargetURL || window.webContents.getURL()
|
|
|
|
|
void showErrorPage(window, context, targetURL, `${errorCode}: ${errorDescription}`)
|
2026-04-19 14:18:20 +08:00
|
|
|
},
|
2026-05-01 20:39:09 +08:00
|
|
|
)
|
2026-04-19 14:18:20 +08:00
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
window.webContents.on('did-finish-load', () => {
|
|
|
|
|
const state = navigationStateRegistry.get(window)
|
2026-04-19 14:18:20 +08:00
|
|
|
if (!state) {
|
2026-05-01 20:39:09 +08:00
|
|
|
return
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
const currentURL = window.webContents.getURL()
|
2026-05-12 01:31:50 +08:00
|
|
|
clearLoadTimeoutWatchdog(state)
|
|
|
|
|
const isLocalPage = isLocalPageURL(currentURL)
|
|
|
|
|
state.readyForDetection = !isLocalPage
|
|
|
|
|
if (!isLocalPage) {
|
|
|
|
|
state.localPageKind = null
|
|
|
|
|
}
|
2026-04-19 14:18:20 +08:00
|
|
|
if (state.readyForDetection) {
|
2026-05-01 20:39:09 +08:00
|
|
|
state.lastMainFrameError = null
|
2026-05-08 22:12:28 +08:00
|
|
|
scheduleBlankPageWatchdog(window, state)
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
})
|
2026-04-19 14:18:20 +08:00
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
window.webContents.on('render-process-gone', (_event, details) => {
|
|
|
|
|
const state = navigationStateRegistry.get(window)
|
2026-04-19 14:18:20 +08:00
|
|
|
if (state) {
|
2026-05-01 20:39:09 +08:00
|
|
|
state.readyForDetection = false
|
2026-05-12 01:31:50 +08:00
|
|
|
clearNavigationWatchdogs(state)
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
console.error(`[desktop-window] ${context} renderer gone`, details)
|
|
|
|
|
})
|
2026-05-08 22:12:28 +08:00
|
|
|
|
|
|
|
|
window.once('closed', () => {
|
|
|
|
|
const state = navigationStateRegistry.get(window)
|
|
|
|
|
if (state) {
|
2026-05-12 01:31:50 +08:00
|
|
|
clearNavigationWatchdogs(state)
|
2026-05-08 22:12:28 +08:00
|
|
|
}
|
|
|
|
|
})
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function isWindowReadyForDetection(window: BrowserWindow): boolean {
|
2026-05-01 20:39:09 +08:00
|
|
|
const state = navigationStateRegistry.get(window)
|
2026-04-19 14:18:20 +08:00
|
|
|
if (!state) {
|
2026-05-12 01:31:50 +08:00
|
|
|
return !isLocalPageURL(window.webContents.getURL())
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
2026-05-01 20:39:09 +08:00
|
|
|
return state.readyForDetection
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function loadWindowURLSafely(
|
|
|
|
|
window: BrowserWindow,
|
|
|
|
|
targetURL: string,
|
|
|
|
|
context: string,
|
|
|
|
|
): Promise<void> {
|
2026-05-12 01:31:50 +08:00
|
|
|
await navigateRemoteURL(window, targetURL, context, {
|
|
|
|
|
resetBlankRetry: true,
|
|
|
|
|
showLoadingPage: true,
|
|
|
|
|
})
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|