Files
geo/apps/desktop-client/src/main/external-window.ts
T
root b16e9f0bd1 feat(desktop): implement workspace foundation + desktop-client skeleton
Plan 0 (workspaces): add workspaces + workspace_memberships schema, extend
JWT/Actor/claims with primary_workspace_id, seed default workspace per tenant,
thread workspace_id through tenant monitoring quota.

Plan A (desktop skeleton): new Electron app (apps/desktop-client) with main/
preload/renderer, shared Vue component package (packages/ui-shared), and server
surface — desktop client registration + token rotation + heartbeat, SSE task
event stream, desktop accounts/tasks/content handlers, publish job endpoint,
and supporting repositories, services, sqlc queries, and migrations.

Hard cutover per plan: remove browser-extension monitoring callback endpoints,
stub legacy media API in admin-web, and delete monitoring_callback_handler.go.
2026-04-19 14:18:20 +08:00

226 lines
6.3 KiB
TypeScript

import type { BrowserWindow } from "electron/main";
import { STANDARD_USER_AGENT } from "./user-agent";
const ignoredNavigationFailurePattern = /ERR_ABORTED|ERR_BLOCKED_BY_CLIENT/i;
const errorPagePrefix = "data:text/html";
interface WindowNavigationState {
readyForDetection: boolean;
lastMainFrameError: string | null;
}
const navigationStateRegistry = new WeakMap<BrowserWindow, WindowNavigationState>();
function escapeHtml(value: string): string {
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll("\"", "&quot;")
.replaceAll("'", "&#39;");
}
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;
}
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 24px;
background:
radial-gradient(circle at top left, rgba(15, 118, 110, 0.12), transparent 28%),
radial-gradient(circle at top right, rgba(37, 99, 235, 0.1), transparent 24%),
linear-gradient(180deg, #eef3f8, #e4ebf3);
color: #0f172a;
}
.card {
width: min(680px, 100%);
padding: 24px;
border-radius: 24px;
border: 1px solid rgba(15, 23, 42, 0.08);
background: rgba(255, 255, 255, 0.88);
box-shadow: 0 18px 42px rgba(15, 23, 42, 0.12);
}
h1, p, pre {
margin: 0;
}
h1 {
font-size: 28px;
letter-spacing: -0.04em;
}
p {
margin-top: 12px;
line-height: 1.7;
color: #475569;
}
pre {
margin-top: 16px;
padding: 14px 16px;
border-radius: 16px;
background: rgba(15, 23, 42, 0.05);
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
}
.actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 18px;
}
button,
a {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
padding: 0 16px;
border-radius: 12px;
border: 1px solid rgba(15, 23, 42, 0.08);
background: white;
color: #0f172a;
text-decoration: none;
font: inherit;
font-weight: 700;
cursor: pointer;
}
.primary {
background: linear-gradient(180deg, #44a7ff, #1b6cff);
border-color: rgba(27, 108, 255, 0.18);
color: white;
}
</style>
</head>
<body>
<main class="card">
<h1>授权页加载失败</h1>
<p>${escapeHtml(context)} 在打开远程页面时出现网络或 TLS 握手异常。窗口已保留,不再直接闪退。</p>
<p>如果这是偶发网络抖动,点“重新加载”即可;如果持续失败,再看控制台里打印的 URL 和错误码定位。</p>
<pre>${escapeHtml(message)}
${escapeHtml(targetURL)}</pre>
<div class="actions">
<button class="primary" onclick="location.href = ${JSON.stringify(targetURL)}">重新加载</button>
<a href=${JSON.stringify(targetURL)} target="_self">打开目标地址</a>
</div>
</main>
</body>
</html>`;
return `data:text/html;charset=UTF-8,${encodeURIComponent(html)}`;
}
export function attachWindowDiagnostics(window: BrowserWindow, context: string): void {
navigationStateRegistry.set(window, {
readyForDetection: false,
lastMainFrameError: null,
});
window.webContents.on("did-start-loading", () => {
const state = navigationStateRegistry.get(window);
if (!state) {
return;
}
state.readyForDetection = false;
});
window.webContents.on(
"did-fail-load",
(_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
if (!isMainFrame || errorCode === -3) {
return;
}
const state = navigationStateRegistry.get(window);
if (state) {
state.readyForDetection = false;
state.lastMainFrameError = `${errorCode}:${errorDescription}`;
}
console.warn(`[desktop-window] ${context} main-frame load failed`, {
errorCode,
errorDescription,
validatedURL,
});
},
);
window.webContents.on("did-finish-load", () => {
const state = navigationStateRegistry.get(window);
if (!state) {
return;
}
const currentURL = window.webContents.getURL();
state.readyForDetection = !currentURL.startsWith(errorPagePrefix);
if (state.readyForDetection) {
state.lastMainFrameError = null;
}
});
window.webContents.on("render-process-gone", (_event, details) => {
const state = navigationStateRegistry.get(window);
if (state) {
state.readyForDetection = false;
}
console.error(`[desktop-window] ${context} renderer gone`, details);
});
}
export function isWindowReadyForDetection(window: BrowserWindow): boolean {
const state = navigationStateRegistry.get(window);
if (!state) {
return !window.webContents.getURL().startsWith(errorPagePrefix);
}
return state.readyForDetection;
}
export async function loadWindowURLSafely(
window: BrowserWindow,
targetURL: string,
context: string,
): Promise<void> {
try {
await window.loadURL(targetURL, { userAgent: STANDARD_USER_AGENT });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (ignoredNavigationFailurePattern.test(message) || window.isDestroyed()) {
return;
}
console.error(`[desktop-window] ${context} loadURL failed`, {
targetURL,
message,
});
if (window.webContents.getURL().startsWith(errorPagePrefix)) {
return;
}
try {
await window.loadURL(errorPageDataURL(context, targetURL, message));
} catch (fallbackError) {
const fallbackMessage =
fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
console.error(`[desktop-window] ${context} error-page loadURL failed`, {
targetURL,
message: fallbackMessage,
});
}
}
}